1<!DOCTYPE html> 2<html lang="bo"> 3<head>
vendor: 69 bytes, lines 3-4
3 4 <script async src="https://www.googletagmanager.com/gtag/js?id=
4G-WQ62DL1S2M
vendor: 17 bytes, lines 4-5
4"></script> 5
5<script> 6
vendor: 148 bytes, lines 6-9
6window.dataLayer = window.dataLayer || []; 7 function gtag(){dataLayer.push(arguments);} 8 gtag('js', new Date()); 9 gtag('config', '
9G-WQ62DL1S2M
vendor: 9 bytes, lines 9-10
9'); 10
10</script>
10 11
11<script src="timer.js"></script>
11 12 13 <meta charset="UTF-8"> 14 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 15 <title>TibetanKeys | Timer Room</title> 16 <link rel="icon" type="image/png" href="og.png"> 17 <style> 18 @font-face { font-family: 'OuChan2'; src: url('Monlam Uni OuChan2.ttf'); } 19 @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@700;900&display=swap'); 20 * { box-sizing: border-box; } 21 *:focus, *:focus-visible { outline: none !important; } 22 :root { 23 --bg:#000000; --accent:#d4af37; --text:#ffffff; 24 --error:#ff4d4d; --success:#00e676; --blue-glow:#00ffff; 25 --cursor-bg:rgba(0,255,255,.15); --cursor-border:rgba(0,255,255,.5); 26 --frost-cyan:#00ffff; 27 } 28 body,html { margin:0;padding:0;background:var(--bg);color:var(--text);font-family:'Segoe UI',sans-serif;height:100vh;width:100vw;max-height:100vh;max-width:100vw;overflow:hidden !important;display:flex;flex-direction:column;user-select:none;overscroll-behavior:none; } 29 30 body.layout-medium { --dynamic-font-size:55px; --line-gap:20px; } 31 body.layout-small { --dynamic-font-size:50px; --line-gap:20px; } 32 body.layout-clean { --dynamic-font-size:60px; --line-gap:20px; } 33 34 /* --- REMOVE SPINNERS FOR NUMBER INPUTS --- */ 35 input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } 36 input[type=number] { -moz-appearance: textfield; } 37 38 /* GLITCH */ 39 @keyframes chromaticGlitch { 40 0% {filter:none;transform:none;} 41 12% {filter:drop-shadow(-5px 0 0 rgba(255,0,0,.85)) drop-shadow(5px 0 0 rgba(0,255,255,.85));transform:translate(-2px,1px) skewX(-1.5deg);} 42 24% {filter:drop-shadow(4px 0 0 rgba(255,0,0,.6)) drop-shadow(-4px 0 0 rgba(0,255,255,.6));transform:translate(3px,-1px);} 43 38% {filter:drop-shadow(-3px 0 0 rgba(255,0,0,.5)) drop-shadow(3px 0 0 rgba(0,255,255,.5)) brightness(1.15);transform:translate(-1px,2px) skewX(1deg);} 44 55% {filter:drop-shadow(2px 0 0 rgba(255,0,0,.3)) drop-shadow(-2px 0 0 rgba(0,255,255,.3));transform:translate(1px,0);} 45 75% {filter:drop-shadow(-1px 0 0 rgba(255,0,0,.15)) drop-shadow(1px 0 0 rgba(0,255,255,.15));transform:translate(-1px,0);} 46 100%{filter:none;transform:none;} 47 } 48 body.chromatic-glitch { animation:chromaticGlitch .55s steps(1) forwards; } 49 .glitch-band { position:fixed;left:0;width:100%;pointer-events:none;z-index:9001;animation:bandFlash .25s ease-out forwards; } 50 @keyframes bandFlash { 0%{opacity:.85;} 60%{opacity:.4;} 100%{opacity:0;} } 51 #glitch-container { position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9001;overflow:hidden; } 52 53 /* PRELOADER - Fixed display:none to prevent blocking setup */ 54 #preloader { position:fixed;top:0;left:0;width:100%;height:100%;background:#000;z-index:20000;display:none;flex-direction:column;justify-content:center;align-items:center;transition:opacity .5s ease-out; } 55 .loader-dots { display:flex;gap:12px; } 56 .dot { width:15px;height:15px;background-color:var(--accent);border-radius:50%;box-shadow:0 0 10px rgba(212,175,55,.5);animation:bounce 1.4s infinite ease-in-out both; } 57 .dot:nth-child(1){animation-delay:-.32s;}.dot:nth-child(2){animation-delay:-.16s;}.dot:nth-child(3){animation-delay:0s;}.dot:nth-child(4){animation-delay:.16s;}.dot:nth-child(5){animation-delay:.32s;} 58 @keyframes bounce { 0%,80%,100%{transform:scale(0);opacity:.3;} 40%{transform:scale(1.2);opacity:1;box-shadow:0 0 20px var(--accent);} } 59 60 /* SETUP OVERLAY */ 61 #setup-overlay { position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);backdrop-filter:blur(10px);z-index:15000;display:flex;justify-content:center;align-items:center; } 62 #setup-panel { background:#0a0f18;border:1px solid rgba(212,175,55,.3);padding:50px 70px;border-radius:24px;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,.9), 0 0 0 1px rgba(212,175,55,.1); } 63 .setup-title { color:var(--accent);margin:0 0 25px 0;font-size:22px;letter-spacing:6px;text-transform:uppercase;font-weight:900;text-shadow:0 0 20px rgba(212,175,55,.4); } 64 .time-presets { display:flex;gap:15px;margin-bottom:30px;justify-content:center; } 65 .time-btn { background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.1);color:#fff;padding:12px 24px;border-radius:10px;cursor:pointer;font-size:1rem;font-weight:bold;transition:0.3s; } 66 .time-btn:hover, .time-btn.selected { background:var(--accent);color:#000;border-color:var(--accent);box-shadow:0 0 20px rgba(212, 175, 55, 0.5);transform:translateY(-2px); } 67 .custom-time-box { display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:35px;font-size:1.4rem;color:#888;font-weight:900; } 68 .custom-input { width:80px;padding:12px;text-align:center;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.1);color:var(--accent);font-size:1.4rem;border-radius:10px;font-family:'Roboto Mono',monospace;transition:0.3s; } 69 .custom-input:focus { border-color:var(--accent);box-shadow:0 0 15px rgba(212,175,55,0.2); } 70 .start-btn { background:var(--accent);color:#000;border:none;padding:16px 60px;font-size:1.2rem;letter-spacing:2px;text-transform:uppercase;font-weight:900;border-radius:50px;cursor:pointer;box-shadow:0 0 20px rgba(212, 175, 55, 0.3);transition:all 0.3s; } 71 .start-btn:hover { transform:scale(1.05);box-shadow:0 0 40px rgba(212, 175, 55, 0.6); } 72 73 /* HUD */ 74 .hud { width:100%;height:64px;padding:0 16px;background:rgba(8,8,10,.98);border-bottom:1px solid rgba(212,175,55,.12);flex-shrink:0;z-index:200;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:0 12px; } 75 .hud-left { display:flex;align-items:center;gap:5px;justify-self:start; } 76 .level-info h1 { margin:0;font-size:11px;letter-spacing:3px;color:var(--accent);text-transform:uppercase;font-weight:700;white-space:nowrap;margin-right:15px; } 77 .icon-btn { background:none;border:none;color:#666;cursor:pointer;width:38px;height:38px;display:flex;justify-content:center;align-items:center;border-radius:8px;transition:all .2s ease;position:relative; } 78 .icon-btn:hover { color:#fff;background:rgba(255,255,255,.06); } 79 .icon-btn.active { color:#fff !important; } 80 .icon-btn svg { width:18px;height:18px;fill:currentColor; } 81 82 /* Kb popover */ 83 .kb-menu-wrap { position:relative; } 84 #kb-popover { display:none;position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%);background:rgba(10,10,14,.98);border:1px solid rgba(212,175,55,.2);border-radius:14px;padding:10px;width:170px;z-index:500;box-shadow:0 20px 60px rgba(0,0,0,.8),0 0 0 1px rgba(212,175,55,.05);backdrop-filter:blur(20px); } 85 #kb-popover::before { content:'';position:absolute;top:-6px;left:50%;width:10px;height:10px;background:rgba(10,10,14,.98);border-left:1px solid rgba(212,175,55,.2);border-top:1px solid rgba(212,175,55,.2);transform:translateX(-50%) rotate(45deg); } 86 #kb-popover.open { display:block;animation:popoverIn .18s ease-out; } 87 @keyframes popoverIn { from{opacity:0;transform:translateX(-50%) translateY(-6px);} to{opacity:1;transform:translateX(-50%) translateY(0);} } 88 .kb-menu-title { font-size:9px;letter-spacing:3px;color:#444;text-transform:uppercase;margin-bottom:8px;font-weight:700;padding:0 4px; } 89 .kb-menu-row { display:flex;align-items:center;justify-content:space-between;padding:9px 12px;border-radius:8px;
89cursor:pointer;transition:all .15s;margin-bottom:3px;border:1px solid transparent; } 90 .kb-menu-row:hover { background:rgba(255,255,255,.06); } 91 .kb-menu-row.kb-active { background:rgba(212,175,55,.15);border-color:rgba(212,175,55,.3); } 92 .kb-menu-row.kb-active .kb-menu-row-label { color:var(--accent);font-weight:700; } 93 .kb-menu-row.kb-active .kb-menu-row-check { color:var(--accent); } 94 .kb-menu-row-label { font-size:11px;color:#888;letter-spacing:1px;transition:color .15s; } 95 .kb-menu-row-check { font-size:12px;color:transparent;transition:color .15s; } 96 97 /* live WPM/ACC & TIMER */ 98 .hud-center { display:flex;align-items:center;justify-content:center; } 99 .hud-live { display:flex;align-items:center;gap:35px; } 100 .hud-live-stat { display:flex;align-items:baseline;gap:6px; } 101 .hud-live-val { font-size:24px;font-weight:800;color:var(--blue-glow);text-shadow:0 0 14px rgba(0,255,255,.4); font-variant-numeric: tabular-nums; } 102 .hud-live-label { font-size:10px;letter-spacing:2px;color:#666;text-transform:uppercase;font-weight:700; } 103 .timer-accent { color:var(--accent); text-shadow:0 0 14px rgba(212,175,55,.4); font-size: 28px; } 104 105 .phase-indicator { justify-self:end;font-size:11px;letter-spacing:3px;color:rgba(255,255,255,0.4);text-transform:uppercase;font-weight:bold;white-space:nowrap; } 106 .size-controls { display:flex;align-items:center;gap:2px;background:rgba(255,255,255,.04);padding:3px;border-radius:8px;border:1px solid rgba(255,255,255,.1);height:36px;margin-left:6px; } 107 .size-btn { background:none;border:none;color:#666;font-size:12px;font-weight:bold;cursor:pointer;width:30px;height:100%;display:flex;justify-content:center;align-items:center;transition:.2s;border-radius:5px; } 108 .size-btn:hover { background:rgba(255,255,255,.1);color:#fff; } 109 110 #typing-progress-bar { width:0%;height:3px;background:var(--accent);flex-shrink:0;transition:width 1s linear;z-index:199; } 111 112 /* ââ STAGE ââ */ 113 .stage { flex:1;display:flex;flex-direction:column;width:100%;overflow:hidden;position:relative;justify-content:center;align-items:center;opacity:0;transition:opacity .8s ease-in;min-height:0; } 114 .stanza-scroll { flex:1; width:100%; display:flex; flex-direction:column; align-items:center; justify-content:flex-start; overflow-y:auto; overflow-x:hidden; -ms-overflow-style:none; scrollbar-width:none; min-height:0; } 115 .stanza-scroll::-webkit-scrollbar { display:none; } 116 .stanza-scroll::before, .stanza-scroll::after { content:''; margin:auto; } 117 118 #stanza-container { display:flex; justify-content:center; width:100%; padding:20px 40px 60px 40px; flex-shrink:0; } 119 120 .paragraph-wrapper { display:flex; flex-wrap:wrap; justify-content:flex-start; align-content:flex-start; max-width:1300px; padding:1em 30px; font-family:'OuChan2',serif; font-size:var(--dynamic-font-size); line-height:1.8; } 121 .tib-word { display:flex; flex-wrap:nowrap; } 122 123 .char { opacity:.4;transition:all .15s ease;position:relative;display:flex;justify-content:center;align-items:center;text-align:center;padding:0 1px;margin:0;border-radius:4px;border:1px solid transparent;min-height:1.2em; } 124 .char.tsek { padding:0;margin:0; } 125 .sentence-break { margin-right:1.2em; } 126 .char.space-char { width: 0.8em; } 127 128 .char .ghost-text { position:absolute;top:0;left:0;right:0;bottom:0;display:flex;justify-content:center;align-items:center;color:var(--blue-glow);font-family:'OuChan2',serif;font-size:inherit;white-space:nowrap;pointer-events:none;z-index:5; } 129 .char .ghost-text.error { color:var(--error); } 130 .char.current { opacity:1;color:#fff;transform:scale(1.1);z-index:10;background:transparent;border-color:transparent;box-shadow:none;text-shadow:0 0 15px rgba(255,255,255,.8); } 131 .char.correct { opacity:.7;color:#fff;text-shadow:none; } 132 .char.wrong { opacity:1;color:var(--error);animation:shake .3s; } 133 @keyframes shake { 0%,100%{transform:translateX(0);} 25%{transform:translateX(-5px);} 75%{transform:translateX(5px);} } 134 135 /* ââ KEYBOARD ââ */ 136 #keyboard-wrapper { width:100%;max-width:900px;aspect-ratio:950/400;margin:0 auto;flex-shrink:0;position:relative;z-index:20;transition:all .5s; } 137 body.layout-medium #keyboard-wrapper { height:32vh;opacity:1;margin-bottom:16px; } 138 body.layout-small #keyboard-wrapper { height:24vh;opacity:1;margin-bottom:16px; } 139 body.layout-clean #keyboard-wrapper { height:0;opacity:0;pointer-events:none;margin-bottom:0; } 140 body.hands-hidden #layer-hands-container { display:none; } 141 #layer-keyboard { position:absolute;top:0;left:0;width:100%;height:100%;display:block;filter:drop-shadow(0 -20px 40px rgba(0,0,0,.8));z-index:1; } 142 #layer-hands-container { position:absolute;top:0;left:0;width:100%;height:100%;z-index:2;pointer-events:none; } 143 #layer-hands-container svg { width:100%;height:100%;display:block; } 144 #active-finger { fill:url(#finger-gradient) !important;stroke:#00ffff !important;stroke-width:.5px !important;filter:drop-shadow(0 0 10px #00ffff) drop-shadow(0 0 20px #00ffff) !important;opacity:1 !important; } 145 #layer-hands-container svg g:not(#active-finger),#layer-hands-container svg path:not(#active-finger) { opacity:.5; } 146 .st0 { fill:#d4af37;font-family:Arial,sans-serif;font-size:14px;font-weight:700;pointer-events:none; } 147 .st1 { fill:none;opacity:.5;
147stroke:#d4af37;stroke-width:1.5px;transition:all .1s; } 148 .st2 { fill:transparent; } 149 .key-guide { stroke:var(--blue-glow) !important;stroke-width:3px !important;fill:rgba(0,255,255,.15) !important;opacity:1 !important;filter:drop-shadow(0 0 15px var(--blue-glow)); } 150 .key-hit { fill:var(--success) !important;stroke:var(--success) !important;filter:drop-shadow(0 0 15px var(--success));transition:0s; } 151 152 /* ââ RESULT MODAL ââ */ 153 #result-modal { display:none;position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;justify-content:center;align-items:center;overflow:hidden;background:#000; } 154 #result-aurora { position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;overflow:hidden; } 155 .aurora-blob { position:absolute;border-radius:50%;filter:blur(80px);animation:auroraFloat 8s ease-in-out infinite alternate; } 156 .ab1{width:60vw;height:60vw;background:radial-gradient(circle,rgba(0,230,118,.18),transparent 70%);top:-15%;left:-10%;animation-delay:0s;} 157 .ab2{width:50vw;height:50vw;background:radial-gradient(circle,rgba(0,200,255,.15),transparent 70%);top:20%;right:-5%;animation-delay:-2s;} 158 .ab3{width:45vw;height:45vw;background:radial-gradient(circle,rgba(212,175,55,.12),transparent 70%);bottom:-10%;left:20%;animation-delay:-4s;} 159 .ab4{width:40vw;height:40vw;background:radial-gradient(circle,rgba(196,113,237,.10),transparent 70%);bottom:5%;right:10%;animation-delay:-6s;} 160 @keyframes auroraFloat{0%{transform:scale(1) translate(0,0);}100%{transform:scale(1.2) translate(3%,5%);}} 161 #result-canvas { position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2; } 162 163 .victory-container { display:flex;flex-direction:column;align-items:center;position:relative;z-index:3;padding:0 20px;gap:20px; width:min(480px, 90vw); text-align:center;} 164 .res-title { font-size:20px;letter-spacing:6px;text-transform:uppercase;font-weight:900;color:var(--accent);text-shadow:0 0 30px rgba(212,175,55,.8),0 0 60px rgba(212,175,55,.3);margin-bottom:10px;animation:titleGlow 2s ease-in-out infinite alternate; } 165 @keyframes titleGlow{from{letter-spacing:6px;}to{letter-spacing:8px;text-shadow:0 0 40px rgba(212,175,55,1),0 0 80px rgba(212,175,55,.5);}} 166 .res-grid { display:flex;gap:15px;width:100%; justify-content:center;} 167 .res-stat { flex:1;padding:25px 10px 20px;border-radius:16px;border:1px solid transparent;text-align:center;position:relative;overflow:hidden;opacity:0;transform:translateY(30px) scale(.9);transition:opacity .5s ease,transform .5s cubic-bezier(.175,.885,.32,1.275),box-shadow .3s; } 168 .res-stat.visible { opacity:1;transform:translateY(0) scale(1); } 169 .res-stat::before { content:'';position:absolute;top:0;left:0;right:0;bottom:0;border-radius:16px;opacity:.06; } 170 .res-stat.stat-wpm { background:linear-gradient(160deg,rgba(0,40,50,.9),rgba(0,20,28,.95));border-color:rgba(0,255,255,.25);box-shadow:0 0 30px rgba(0,255,255,.08),inset 0 1px 0 rgba(0,255,255,.1); } 171 .res-stat.stat-wpm::before { background:var(--blue-glow); } 172 .res-stat.stat-acc { background:linear-gradient(160deg,rgba(0,40,20,.9),rgba(0,20,10,.95));border-color:rgba(0,230,118,.25);box-shadow:0 0 30px rgba(0,230,118,.08),inset 0 1px 0 rgba(0,230,118,.1); } 173 .res-stat.stat-acc::before { background:var(--success); } 174 175 .res-val { font-size:clamp(32px,5vw,56px);font-weight:900;font-variant-numeric:tabular-nums;line-height:1;margin-bottom:8px;display:block;white-space:nowrap; } 176 .stat-wpm .res-val { color:var(--blue-glow);text-shadow:0 0 20px rgba(0,255,255,.7),0 0 40px rgba(0,255,255,.3); } 177 .stat-acc .res-val { color:var(--success);text-shadow:0 0 20px rgba(0,230,118,.7),0 0 40px rgba(0,230,118,.3); } 178 .res-icon { font-size:22px;margin-bottom:8px;display:block;opacity:.7; } 179 .res-label { font-size:9px;color:rgba(255,255,255,.3);letter-spacing:3px;text-transform:uppercase;font-weight:700;margin-top:4px; } 180 @keyframes numBoom{0%{transform:scale(0) rotate(-5deg);opacity:0;}60%{transform:scale(1.2) rotate(1deg);}80%{transform:scale(.95);}100%{transform:scale(1);opacity:1;}} 181 .res-val.boom { animation:numBoom .6s cubic-bezier(.175,.885,.32,1.275) both; } 182 183 .timer-recap { font-size: 14px; color: #aaa; letter-spacing: 2px; text-transform: uppercase; margin-top: 10px; margin-bottom: 20px; } 184 .timer-recap span { color: var(--accent); font-weight: bold; } 185 186 .prompt-container { display:flex; justify-content:center; gap:30px; width:100%; animation:pulse 2s infinite; margin-top: 15px; } 187 .prompt-group { display:flex; flex-direction:column; align-items:center; gap:8px; } 188 .prompt-key { color:#fff;font-weight:800;font-size:11px;letter-spacing:2px;text-transform:uppercase;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05);padding:8px 16px;border-radius:8px;min-width:80px;text-align:center;cursor:pointer;transition:all .2s;pointer-events:auto;backdrop-filter:blur(10px); white-space:nowrap; } 189 .prompt-key:hover { border-color:var(--accent);color:var(--accent);background:rgba(212,175,55,.1);transform:translateY(-2px); } 190 .prompt-key:active { transform:scale(.95); } 191 .shining-btn { border-color:var(--accent);color:var(--accent);background:rgba(212,175,55,.15);animation:pulseGlow 2s infinite; } 192 @keyframes pulseGlow{0%{box-shadow:0 0 10px rgba(212,175,55,.4);}50%{box-shadow:0 0 25px rgba(212,175,55,.8);}100%{box-shadow:0 0 10px rgba(212,175,55,.4);}} 193 .prompt-action { color:rgba(255,255,255,.2);font-size:8px;letter-spacing:2px;text-transform:uppercase;text-align:center; } 194 @keyframes pulse { 0%{opacity:.55;} 50%{opacity:1;} 100%{opacity:.55;} } 195 196 .back-btn { position:fixed;bottom:20px;left:30px;color:#666;text-decoration:none;font-size:11px;letter-spacing:2px;text-transform:uppercase;transition:.3s;z-index:50;display:flex;align-items:center;gap:8px;pointer-events:auto; } 197 .back-btn:hover { color:var(--accent); } 198 </style> 199</head> 200<body class="layout-small"> 201 202<div id="setup-overlay"> 203 <div id="setup-panel"> 204 <h2 class="setup-title">Set Timer</h2> 205 <div class="time-presets"> 206 <button class="time-btn" onclick="selectTime(30, this)">30s</button> 207 <button class="time-btn" onclick="selectTime(60, this)">1m</button> 208 <button class="time-btn selected" onclick="selectTime(120, this)">2m</button> 209 <button class="time-btn" onclick="selectTime(300, this)">5m</button> 210 <button class="time-btn" onclick="selectTime(600, this)">10m</button> 211 </div> 212 <div class="custom-time-box"> 213 <input type="number" id="cust-min" class="custom-input" placeholder="00" min="0" max="60" onin
213put="clearPresets()"> 214 <span>m :</span> 215 <input type="number" id="cust-sec" class="custom-input" placeholder="00" min="0" max="59" oninput="clearPresets()"> 216 <span>s</span> 217 </div> 218 <button class="start-btn" onclick="initGame()">ENTER ARENA</button> 219 </div> 220</div> 221 222<div id="preloader"><div class="loader-dots"><div class="dot"></div><div class="dot"></div><div class="dot"></div><div class="dot"></div><div class="dot"></div></div></div> 223<div id="glitch-container"></div> 224 225<svg style="width:0;height:0;position:absolute;" aria-hidden="true" focusable="false"> 226 <defs><linearGradient id="finger-gradient" x1="0%" y1="0%" x2="0%" y2="100%"> 227 <stop offset="0%" style="stop-color:#00ffff;stop-opacity:0.6"/> 228 <stop offset="50%" style="stop-color:#00ccff;stop-opacity:0.4"/> 229 <stop offset="90%" style="stop-color:#00ffff;stop-opacity:0"/> 230 </linearGradient></defs> 231</svg> 232 233<div class="hud"> 234 <div class="hud-left"> 235 <div class="level-info"><h1>Speed Test</h1></div> 236 <button class="icon-btn" onclick="window.location.reload()" title="Restart"><svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg></button> 237 <button class="icon-btn active" id="btn-hand" onclick="toggleHands()"><svg viewBox="0 0 24 24"><path d="M23 5.5V20c0 2.2-1.8 4-4 4h-7.3c-1.08 0-2.1-.43-2.85-1.19L1 14.83s1.26-1.23 1.3-1.25c.22-.19.49-.29.79-.29.22 0 .42.06.6.16.04.01 4.31 2.46 4.31 2.46V4c0-.83.67-1.5 1.5-1.5S11 3.17 11 4v7h1V1.5c0-.83.67-1.5 1.5-1.5S15 .67 15 1.5V11h1V2.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5V11h1V5.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5V11h1V5.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5z"/></svg></button> 238 239 <div class="kb-menu-wrap"> 240 <button class="icon-btn" id="btn-keyboard" onclick="toggleKbMenu()" title="Keyboard Size"><svg viewBox="0 0 24 24"><path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm9 7H8v-2h8v2zm0-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z"/></svg></button> 241 <div id="kb-popover"> 242 <div class="kb-menu-title">Keyboard Size</div> 243 <div class="kb-menu-row" id="kb-row-medium" onclick="setLayout('layout-medium',true);closeKbMenu()"><span class="kb-menu-row-label">Medium</span><span class="kb-menu-row-check">â</span></div> 244 <div class="kb-menu-row" id="kb-row-small" onclick="setLayout('layout-small',true);closeKbMenu()"><span class="kb-menu-row-label">Small</span><span class="kb-menu-row-check">â</span></div> 245 <div class="kb-menu-row" id="kb-row-clean" onclick="setLayout('layout-clean',true);closeKbMenu()"><span class="kb-menu-row-label">Off</span><span class="kb-menu-row-check">â</span></div> 246 </div> 247 </div> 248 <button id="sound-btn" class="icon-btn" onclick="sfx.toggle()"><svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg></button> 249 <div class="size-controls"> 250 <button class="size-btn" onclick="adjustFontSize(-5)">A-</button> 251 <button class="size-btn" onclick="adjustFontSize(5)">A+</button> 252 </div> 253 </div> 254 255 <div class="hud-center"> 256 <div class="hud-live"> 257 <div class="hud-live-stat"><span class="hud-live-val timer-accent" id="live-timer">00:00</span></div> 258 <div class="hud-live-stat"><span class="hud-live-val" id="live-wpm">0</span><span class="hud-live-label">WPM</span></div> 259 <div class="hud-live-stat"><span class="hud-live-val" id="live-acc">100%</span><span class="hud-live-label">ACC</span></div> 260 </div> 261 </div> 262 <div class="phase-indicator">Begin typing to start</div> 263</div> 264 265<div id="typing-progress-bar"></div> 266 267<div class="stage"> 268 <div class="stanza-scroll" id="stanza-scroll"> 269 <div id="stanza-container"></div> 270 </div> 271 272 <div id="keyboard-wrapper"> 273 <svg id="layer-keyboard" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 950 400" preserveAspectRatio="xMidYMid meet"> 274 <rect class="st2" width="950" height="400"/> 275 <g id="row-1"><g><rect class="st1" x="20" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(43.79 52)">~</text></g><g id="key-1"><rect class="st1" x="80" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(103.94 52)">1</text></g><g id="key-2"><rect class="st1" x="140" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(163.94 52)">2</text></g><g id="key-3"><rect class="st1" x="200" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(223.94 52)">3</text></g><g id="key-4"><rect class="st1" x="260" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(283.94 52)">4</text></g><g id="key-5"><rect class="st1" x="320" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(343.94 52)">5</text></g><g id="key-6"><rect class="st1" x="380" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(403.94 52)">6</text></g><g id="key-7"><rect class="st1" x="440" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(463.94 52)">7</text></g><g id="key-8"><rect class="st1" x="500" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(523.94 52)">8</text></g><g id="key-9"><rect class="st1" x="560" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(583.94 52)">9</text></g><g id="key-0"><rect class="st1" x="620" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(643.94 52)">0</text></g><g><rect class="st1" x="680" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(705.17 52)">-</text></g><g><rect class="st1" x="740" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(763.79 52)">=</text></g><g><rect class="st1" x="800" y="20" width="115" height="55" rx="6"/><text class="st0" transform="translate(822.57 52)">BACKSPACE</text></g></g> 276 <g id="row-2"><g><rect class="st1" x="20" y="85" width="85" height="55" rx="6"/><text class="st0" transform="translate(51.1 117)">TAB</text></g><g id="key-Q"><rect class="st1" x="110" y="85" width="55" height="55" rx="6"/>
276<text class="st0" transform="translate(132.72 117)">Q</text></g><g id="key-W"><rect class="st1" x="170" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(191.81 117)">W</text></g><g id="key-E"><rect class="st1" x="230" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(253.33 117)">E</text></g><g id="key-R"><rect class="st1" x="290" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(313.03 117)">R</text></g><g id="key-T"><rect class="st1" x="350" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(373.64 117)">T</text></g><g id="key-Y"><rect class="st1" x="410" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(433.33 117)">Y</text></g><g id="key-U"><rect class="st1" x="470" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(493.03 117)">U</text></g><g id="key-I"><rect class="st1" x="530" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(555.47 117)">I</text></g><g id="key-O"><rect class="st1" x="590" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(612.72 117)">O</text></g><g id="key-P"><rect class="st1" x="650" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(673.33 117)">P</text></g><g id="key-LBRACK"><rect class="st1" x="710" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(735.17 117)">[</text></g><g id="key-RBRACK"><rect class="st1" x="770" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(795.17 117)">]</text></g><g><rect class="st1" x="830" y="85" width="85" height="55" rx="6"/><text class="st0" transform="translate(870.47 117)">\</text></g></g> 277 <g id="row-3"><g><rect class="st1" x="20" y="150" width="105" height="55" rx="6"/><text class="st0" transform="translate(56.72 182)">CAPS</text></g><g id="key-A"><rect class="st1" x="130" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(153.03 182)">A</text></g><g id="key-S"><rect class="st1" x="190" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(213.33 182)">S</text></g><g id="key-D"><rect class="st1" x="250" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(273.03 182)">D</text></g><g id="key-F"><rect class="st1" x="310" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(333.64 182)">F</text><rect class="bump" x="327" y="190" width="20" height="2" rx="1"/></g><g id="key-G"><rect class="st1" x="370" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(392.72 182)">G</text></g><g id="key-H"><rect class="st1" x="430" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(453.03 182)">H</text></g><g id="key-J"><rect class="st1" x="490" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(513.94 182)">J</text><rect class="bump" x="507" y="190" width="20" height="2" rx="1"/></g><g id="key-K"><rect class="st1" x="550" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(573.03 182)">K</text></g><g id="key-L"><rect class="st1" x="610" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(633.64 182)">L</text></g><g id="key-SEMI"><rect class="st1" x="670" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(695.17 182)">;</text></g><g id="key-QUOTE"><rect class="st1" x="730" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(755.69 182)">'</text></g><g><rect class="st1" x="790" y="150" width="125" height="55" rx="6"/><text class="st0" transform="translate(833.36 182)">ENTER</text></g></g> 278 <g id="row-4"><g id="key-LSHIFT"><rect class="st1" x="20" y="215" width="135" height="55" rx="6"/><text class="st0" transform="translate(71.11 247)">SHIFT</text></g><g id="key-Z"><rect class="st1" x="160" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(183.64 247)">Z</text></g><g id="key-X"><rect class="st1" x="220" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(243.33 247)">X</text></g><g id="key-C"><rect class="st1" x="280" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(303.03 247)">C</text></g><g id="key-V"><rect class="st1" x="340" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(363.33 247)">V</text></g><g id="key-B"><rect class="st1" x="400" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(423.03 247)">B</text></g><g id="key-N"><rect class="st1" x="460" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(483.03 247)">N</text></g><g id="key-M"><rect class="st1" x="520" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(542.42 247)">M</text></g><g id="key-COMMA"><rect class="st1" x="580" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(605.47 247)">,</text></g><g id="key-DOT"><rect class="st1" x="640" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(665.47 247)">.</text></g><g id="key-SLASH"><rect class="st1" x="700" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(725.47 247)">/</text></g><g id="key-RSHIFT"><rect class="st1" x="760" y="215" width="155" height="55" rx="6"/><text class="st0" transform="translate(821.11 247)">SHIFT</text></g></g> 279 <g id="row-5"><g id="key-SPACE"><rect class="st1" x="270" y="280" width="400" height="55" rx="6"/><text class="st0" transform="translate(439.54 312)">SPACEBAR</text></g></g> 280 </svg> 281 <div id="layer-hands-container"></div> 282 </div> 283</div> 284 285<div id="result-modal"> 286 <div id="result-aurora"><div class="aurora-blob ab1"></div><div class="aurora-blob ab2"></div><div class="aurora-blob ab3"></div><div class="aurora-blob ab4"></div></div>
287 <canvas id="result-canvas"></canvas> 288 <div id="victory-container" class="victory-container"> 289 <div class="res-title">TIME'S UP!</div> 290 <div class="timer-recap">Session Duration: <span id="res-duration">02:00</span></div> 291 <div class="res-grid"> 292 <div class="res-stat stat-wpm" ><span class="res-icon">â¡</span><div class="res-val" id="res-wpm">0</div><div class="res-label">WPM</div></div> 293 <div class="res-stat stat-acc" ><span class="res-icon">ð¯</span><div class="res-val" id="res-acc">0%</div><div class="res-label">ACCURACY</div></div> 294 </div> 295 <div class="prompt-container" id="prompt-cont"> 296 <div class="prompt-group"> 297 <div class="prompt-key" onclick="window.location.href='index.html'">SPACE</div> 298 <div class="prompt-action">Main Menu</div> 299 </div> 300 <div class="prompt-group"> 301 <div class="prompt-key shining-btn" onclick="window.location.reload()">SHIFT</div> 302 <div class="prompt-action" style="color:var(--accent);font-weight:bold;text-shadow:0 0 5px var(--accent);">Try Again</div> 303 </div> 304 </div> 305 </div> 306</div> 307 308<a href="index.html" class="back-btn">â Main Menu</a> 309
310<script> 311// âââââââââââââââââââââââââââââââââââââââââââââââ 312// SETUP AND TIMER STATE 313// âââââââââââââââââââââââââââââââââââââââââââââââ 314let selectedSeconds = 120; 315let timeRemaining = 0; 316let timerInterval = null; 317let gameStarted = false; // Becomes true on first keystroke 318let targetStacks = [], currentStackIndex = 0, currentTyped = '', isStackingMode = false; 319let startTime = null, errorsCount = 0, charCount = 0; 320let resultState = 'hidden', currentLayout = 'layout-small'; 321let errorTimeout = null, liveStatsInterval = null; 322 323const container = document.getElementById('stanza-container'); 324const handContainer = document.getElementById('layer-hands-container'); 325const preloader = document.getElementById('preloader'); 326 327// Ensure Fallback Text if timer.js fails to load 328if (!window.TK_TIMER_TEXTS || window.TK_TIMER_TEXTS.length === 0) { 329 window.TK_TIMER_TEXTS = ["འà½à½à¼à½à¾³à½²à½à¼à½à½²à¼à½¡à½à¼à½à½¼à½à¼à½à½´à¼à½à½à½¦à¼à½à½ ིà¼à½à¼à½à½¼à½ ིà¼à½à¼à½¡à½´à½£à¼à½à½¼à½à¼à½£à¾à½¼à½à½¦à¼à½à½²à¼à½¢à½²à¼à½à½à½¼à¼à½¦à¼à½à½à½à¼à½à½à¼à½à½à½¦à¼à½¢à½²à¼à½¢à¼à½à½¦à¼à½à½¦à¾à½¼à½¢à¼à½à½ ིà¼à½¡à½´à½£à¼à½à¾²à½´à¼à½à½²à½à¼à½¡à½²à½à¼"]; 330} 331 332function selectTime(sec, btnElement) { 333 selectedSeconds = sec; 334 document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('selected')); 335 if (btnElement) btnElement.classList.add('selected'); 336 document.getElementById('cust-min').value = ""; 337 document.getElementById('cust-sec').value = ""; 338} 339 340function clearPresets() { 341 document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('selected')); 342 selectedSeconds = 0; 343} 344 345function initGame() { 346 const cMin = parseInt(document.getElementById('cust-min').value) || 0; 347 const cSec = parseInt(document.getElementById('cust-sec').value) || 0; 348 if (cMin > 0 || cSec > 0) selectedSeconds = (cMin * 60) + cSec; 349 if (selectedSeconds <= 0) { alert("Please select a valid time."); return; } 350 351 document.getElementById('setup-overlay').style.display = 'none'; 352 353 // Show preloader while processing text and images 354 preloader.style.display = 'flex'; 355 preloader.style.opacity = '1'; 356 357 timeRemaining = selectedSeconds; 358 updateTimerDisplay(); 359 360 // Clear DOM and Arrays 361 container.innerHTML = ''; 362 targetStacks = []; 363 currentStackIndex = 0; 364 365 appendMoreText(); 366 preloadAssets(); 367} 368 369// âââââââââââââââââââââââââââââââââââââââââââââââ 370// PREFS 371// âââââââââââââââââââââââââââââââââââââââââââââââ 372const PREF_HANDS = 'tk_pref_hands', PREF_SOUND = 'tk_pref_sound', PREF_TEXT_SIZE = 'tk_pref_text_size'; 373let currentFontSize = 50; 374 375function toggleHands(save = true) { document.body.classList.toggle('hands-hidden'); document.getElementById('btn-hand').classList.toggle('active'); if (save) localStorage.setItem(PREF_HANDS, document.body.classList.contains('hands-hidden')); } 376function adjustFontSize(d) { currentFontSize = Math.max(15, Math.min(100, currentFontSize + d)); localStorage.setItem(PREF_TEXT_SIZE, currentFontSize); document.body.style.setProperty('--dynamic-font-size', currentFontSize + 'px'); } 377 378function setLayout(mode, save = true) { 379 document.body.classList.remove('layout-medium','layout-small','layout-clean'); 380 document.body.classList.add(mode); currentLayout = mode; 381 if (save) localStorage.setItem('tk_pref_layout_paragraph', mode); 382 updateKbMenuState(); 383} 384function toggleKbMenu() { const p = document.getElementById('kb-popover'); p.classList.toggle('open'); if (p.classList.contains('open')) updateKbMenuState(); } 385function closeKbMenu() { document.getElementById('kb-popover').classList.remove('open'); } 386function updateKbMenuState() { 387 ['medium','small','clean'].forEach(k => { 388 const r = document.getElementById('kb-row-'+k); 389 if (r) r.classList.toggle('kb-active', currentLayout === 'layout-'+k);
390 }); 391} 392document.addEventListener('click', e => { if (!document.querySelector('.kb-menu-wrap')?.contains(e.target)) closeKbMenu(); }); 393 394// âââââââââââââââââââââââââââââââââââââââââââââââ 395// GLITCH & SOUND 396// âââââââââââââââââââââââââââââââââââââââââââââââ 397const glitchSystem = { 398 container: null, 399 init() { this.container = document.getElementById('glitch-container'); }, 400 trigger() { 401 this.container.innerHTML = ''; 402 for (let i = 0; i < 3 + Math.floor(Math.random() * 6); i++) { 403 const b = document.createElement('div'); b.className = 'glitch-band'; 404 const rgb = Math.random() < .55 ? '255,30,30' : '0,255,255'; 405 b.style.cssText = `top:${Math.random()*96}%;height:${1+Math.random()*18}px;background:rgba(${rgb},${.10+Math.random()*.22});transform:translateX(${(Math.random()-.5)*50}px);animation-delay:${Math.random()*.1}s;animation-duration:${.12+Math.random()*.2}s;`; 406 this.container.appendChild(b); 407 } 408 document.body.classList.add('chromatic-glitch'); 409 setTimeout(() => { this.container.innerHTML = ''; document.body.classList.remove('chromatic-glitch'); }, 450); 410 } 411}; 412function triggerErrorGlitch() { glitchSystem.trigger(); } 413 414const sfx = { 415 ctx: null, muted: false, 416 init() { if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)(); if (this.ctx.state === 'suspended') this.ctx.resume(); }, 417 toggle() { 418 this.muted = !this.muted; localStorage.setItem(PREF_SOUND, this.muted); 419 const btn = document.getElementById('sound-btn'); 420 if (this.muted) { btn.classList.add('muted'); btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73 4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>'; } 421 else { this.init(); btn.classList.remove('muted'); btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>'; } 422 btn.blur(); 423 }, 424 playClick() { if (this.muted || !this.ctx) return; const o = this.ctx.createOscillator(), g = this.ctx.createGain(); o.type='sine'; o.frequency.setValueAtTime(800,this.ctx.currentTime); g.gain.setValueAtTime(.3,this.ctx.currentTime); g.gain.exponentialRampToValueAtTime(.01,this.ctx.currentTime+.08); o.connect(g); g.connect(this.ctx.destination); o.start(); o.stop(this.ctx.currentTime+.08); }, 425 playTick() { if (this.muted || !this.ctx) return; const o = this.ctx.createOscillator(), g = this.ctx.createGain(); o.type='sine'; o.frequency.setValueAtTime(1200,this.ctx.currentTime); g.gain.setValueAtTime(.1,this.ctx.currentTime); g.gain.exponentialRampToValueAtTime(.01,this.ctx.currentTime+.05); o.connect(g); g.connect(this.ctx.destination); o.start(); o.stop(this.ctx.currentTime+.05); }, 426 playError() { if (this.muted || !this.ctx) return; const o = this.ctx.createOscillator(), g = this.ctx.createGain(); o.type='sawtooth'; o.frequency.setValueAtTime(150,this.ctx.currentTime); o.frequency.linearRampToValueAtTime(100,this.ctx.currentTime+.1); g.gain.setValueAtTime(.1,this.ctx.currentTime); g.gain.linearRampToValueAtTime(0,this.ctx.currentTime+.1); o.connect(g); g.connect(this.ctx.destination); o.start(); o.stop(this.ctx.currentTime+.1); } 427}; 428 429// âââââââââââââââââââââââââââââââââââââââââââââââ 430// KEY MAPS 431// âââââââââââââââââââââââââââââââââââââââââââââââ
432const NORMAL_MAP={'k':'à½','t':'à½','p':'à½','s':'ས','d':'à½','f':'à½','g':'à½','h':'ཧ','j':'à½','l':'ལ','z':'à½','x':'à½','c':'འ','v':'à½','b':'à½','n':'à½','m':'à½','q':'à½','w':'à½','e':'ེ','r':'ར','y':'ཡ','u':'ུ','i':'ི','o':'ོ','[':'༷',']':'à¾','\\':'à¼',';':'à½à¾·',"'":'འ',',':'ྱ','.':'ྲ','/':'à¼','`':'à¼','1':'༡','2':'༢','3':'༣','4':'༤','5':'༥','6':'༦','7':'༧','8':'༨','9':'༩','0':'༠','-':'à¾','=':'ཾ',' ':'à¼'}; 433const SHIFT_MAP ={'k':'à½','t':'à½','p':'à½','a':'ཨ','s':'ཤ','d':'à½','f':'à½à¾·','g':'à½à¾·','h':'ྷ','j':'à½à¾·','z':'à½','x':'à½','c':'à½','v':'à½','b':'à½à¾·','n':'à½','m':'ཥ','q':'à½','w':'à¾','e':'ཻ','r':'ཪ','u':'ཱུ','i':'à¾','o':'ཽ','[':'à¾',']':'༵','\\':'ྠ',';':'ཿ',"'":'ཱ','/':'â','`':'༸','1':'à¼','2':'à¼à¼ ','3':'༠','4':'\u201C','5':'\u201D','6':'à¼','7':'༼','8':'༽','9':'(','0':')','-':'ཱà¾','=':'à¾',' ':' '}; 434const SUBJOINED_MAP ={'k':'à¾','g':'à¾','b':'ྦ','t':'à¾','d':'ྡ','p':'ྤ','m':'ྨ','n':'ྣ','l':'ླ','s':'ྶ','h':'ྷ','j':'à¾','c':'à¾','x':'ྩ','f':'à¾','q':'à¾','v':'à¾','w':'à¾','r':'ྲ','y':'ྱ',"'":'ཱ'}; 435const SUBJOINED_SHIFT_MAP={'n':'à¾','d':'ྫ','k':'à¾','t':'ྠ','p':'ྥ','c':'ྪ','x':'à½'}; 436const LEFT_HAND_KEYS = ['`','1','2','3','4','5','q','w','e','r','t','a','s','d','f','g','z','x','c','v','b']; 437const AVAILABLE_SHIFTS = ['sa','sc','sd','sk','sm','sn','sp','sq','ss','st','sv','sw','sx','sz']; 438 439const poseMap={'default':'hands/rest.svg','à¼':'hands/space.svg',' ':'hands/sspace.svg','a':'hands/a.svg'}; 440function generatePoseMap(){ 441 const hasSvg = (k) => /^[a-z]$/i.test(k) || ['.', '/', ',', "'"].includes(k); 442 443 for(let[k,v]of Object.entries(NORMAL_MAP)){ 444 if(v==='à¼'||v===' ')continue; 445 if(!hasSvg(k)) continue; 446 let fn=`hands/${k}.svg`; 447 if(k==='.')fn='hands/rata.svg'; 448 if(k==='/')fn='hands/shea.svg'; 449 poseMap[v]=fn; 450 } 451 for(let[k,v]of Object.entries(SHIFT_MAP)){ 452 if(v===' ')continue; 453 if(!hasSvg(k)) continue; 454 let sn='s'+k,fn=`hands/${sn}.svg`; 455 if(!AVAILABLE_SHIFTS.includes(sn)){ 456 fn=`hands/${k}.svg`; 457 if(k==='.')fn='hands/rata.svg'; 458 if(k==='/')fn='hands/shea.svg'; 459 } 460 poseMap[v]=fn; 461 } 462 for(let[k,v]of Object.entries(SUBJOINED_MAP)){ 463 if(!hasSvg(k)) continue; 464 let fn=`hands/${k}.svg`; 465 if(k==='.')fn='hands/rata.svg'; 466 if(k==='/')fn='hands/shea.svg'; 467 if(!poseMap[v])poseMap[v]=fn; 468 } 469 for(let[k,v]of Object.entries(SUBJOINED_SHIFT_MAP)){ 470 if(!hasSvg(k)) continue; 471 let sn='s'+k,fn=`hands/${sn}.svg`; 472 if(!AVAILABLE_SHIFTS.includes(sn)){ 473 fn=`hands/${k}.svg`; 474 if(k==='.')fn='hands/rata.svg'; 475 if(k==='/')fn='hands/shea.svg'; 476 } 477 if(!poseMap[v])poseMap[v]=fn; 478 } 479 poseMap['ྱ']='hands/,.svg'; 480 poseMap['ྲ']='hands/rata.svg'; 481 poseMap['à¾']='hands/sw.svg'; 482 poseMap['ཱ']="hands/s'.svg"; 483 poseMap['à¼']='hands/shea.svg'; 484} 485generatePoseMap(); 486const svgCache={}; 487 488// âââââââââââââââââââââââââââââââââââââââââââââââ 489// ATOMIC UNITS (FIX) 490// Single keystrokes that output 2+ NON-combining codepoints (e.g. Shift+2 â à¼à¼ ). 491// The default segmenter would wrongly split these into separate stacks, breaking 492// both the keyboard guide and the hit-detection. We protect them as one stack. 493// Derived from the keymaps so any future "one key â multiple base glyphs" entry 494// is covered automatically. 495// âââââââââââââââââââââââââââââââââââââââââââââââ 496const ATOMIC_UNITS = (() => { 497 const set = new Set(); 498 const isMod = cc => (cc>=0x0F71&&cc<=0x0F8D)||(cc>=0x0F90&&cc<=0x0FBC)||cc===0x0F35||cc===0x0F37; 499 [NORMAL_MAP, SHIFT_MAP, SUBJOINED_MAP, SUBJOINED_SHIFT_MAP].forEach(map => {
500 Object.values(map).forEach(v => { 501 if (!v || v.length < 2) return; 502 let bases = 0; 503 for (const ch of v) if (!isMod(ch.charCodeAt(0))) bases++; 504 if (bases >= 2) set.add(v); // 2+ base glyphs = not a normal stack 505 }); 506 }); 507 return set; 508})(); 509 510function shouldEnterStacking(nextChar,currentTyped){ 511 if(currentTyped==='')return false; 512 const DIRECT=new Set(['ྱ','ྲ','à¾','ཱ','à¼','à¼','à¾','ཿ','ཾ','à¾','ཱà¾']); 513 if(DIRECT.has(nextChar))return false; 514 return Object.values(SUBJOINED_MAP).includes(nextChar)||Object.values(SUBJOINED_SHIFT_MAP).includes(nextChar); 515} 516 517// âââââââââââââââââââââââââââââââââââââââââââââââ 518// SEGMENTER (FIX â now respects ATOMIC_UNITS) 519// âââââââââââââââââââââââââââââââââââââââââââââââ 520function segmentWordIntoStacks(word){ 521 let stacks=[], cur='', i=0; 522 while(i < word.length){ 523 // Keep atomic multi-glyph units (like à¼à¼ ) together as ONE stack. 524 let unit = null; 525 for(const u of ATOMIC_UNITS){ 526 if(word.startsWith(u, i) && (!unit || u.length > unit.length)) unit = u; 527 } 528 if(unit){ 529 if(cur!==''){ stacks.push(cur); cur=''; } 530 stacks.push(unit); 531 i += unit.length; 532 continue; 533 } 534 const cc = word.charCodeAt(i); 535 const isMod=(cc>=0x0F71&&cc<=0x0F8D)||(cc>=0x0F90&&cc<=0x0FBC)||cc===0x0F35||cc===0x0F37; 536 if(!isMod){ if(cur!=='') stacks.push(cur); cur=word[i]; } 537 else { cur+=word[i]; } 538 i++; 539 } 540 if(cur!=='') stacks.push(cur); 541 return stacks; 542} 543 544// âââââââââââââââââââââââââââââââââââââââââââââââ 545// DYNAMIC TEXT ENGINE (FIXED APPEND BEHAVIOR) 546// âââââââââââââââââââââââââââââââââââââââââââââââ 547function appendMoreText() { 548 let para = window.TK_TIMER_TEXTS[Math.floor(Math.random() * window.TK_TIMER_TEXTS.length)]; 549 para = para.replace(/[\s\r\n]+/g,' ').trim(); 550 para = para.replace(/à¼+/g,'à¼'); 551 if(!para.endsWith('à¼') && !para.endsWith('à¼')) para += 'à¼'; 552 553 // If we are appending to existing text, prepend a space so it flows naturally 554 if (targetStacks.length > 0 && !para.startsWith(' ')) { 555 para = ' ' + para; 556 } 557 558 let stackCounter = targetStacks.length; // Start from where we left off 559 560 // REUSE existing wrapper instead of creating a new one to prevent split layouts 561 let pWrapper = document.querySelector('.paragraph-wrapper'); 562 if (!pWrapper) { 563 pWrapper = document.createElement('div'); 564 pWrapper.className = 'paragraph-wrapper'; 565 container.appendChild(pWrapper); 566 } 567 568 const stacks = segmentWordIntoStacks(para); 569 let currentWordDiv = document.createElement('div'); 570 currentWordDiv.className = 'tib-word'; 571 572 stacks.forEach(stack => { 573 targetStacks.push({text:stack,type:'char'}); 574 const span = document.createElement('span'); 575 span.innerText = stack; span.className = 'char'; 576 577 if(stack.trim()==='à¼'||stack.trim()==='à¼') span.classList.add('tsek'); 578 if(stack.includes('à¼')) span.classList.add('sentence-break'); 579 if(stack === ' ') span.classList.add('space-char'); 580 581 span.id=`stack-${stackCounter}`; 582 currentWordDiv.appendChild(span); 583 stackCounter++; 584 585 if (stack.includes('à¼') || stack.includes('à¼') || stack === ' ') { 586 pWrapper.appendChild(currentWordDiv); 587 currentWordDiv = document.createElement('div'); 588 currentWordDiv.className = 'tib-word'; 589 } 590 }); 591 592 if (currentWordDiv.childNodes.length > 0) { 593 pWrapper.appendChild(currentWordDiv); 594 } 595 596 // If game has already started, ensure the new elements format correctly 597 if (gameStarted) { 598 renderState(); 599 } 600} 601 602function preloadAssets(){ 603 const unique=new Set(); 604 targetStacks.forEach(s=>{if(s.type==='char'){for(let c of s.text)unique.add(c);}}); 605 const assets=[poseMap['default'],poseMap['à¼'],poseMap[' '],poseMap['a']]; 606 unique.forEach(c=>{if(c==='ལ'){assets.push('hands/a.svg');assets.push('hands/l.svg');}else if(poseMap[c])assets.push(poseMap[c]);});
607 const files=[...new Set(assets)].filter(Boolean); 608 Promise.all(files.map(src=> 609 fetch(src).then(r=>r.ok?r.text():Promise.reject()).then(t=>{ 610 svgCache[src]=t.replace(/<svg([^>]*)>/,'<svg$1 width="100%" height="100%" preserveAspectRatio="xMidYMid meet">'); 611 }).catch(()=>{svgCache[src]='';}) 612 )).then(hidePreloader); 613 setTimeout(hidePreloader,2000); 614} 615 616let preloaderHidden=false; 617let doHideRan = false; 618function hidePreloader(){ 619 if(preloaderHidden)return;preloaderHidden=true; 620 glitchSystem.init(); 621 622 const doHide=()=>{ 623 if(doHideRan) return; 624 doHideRan = true; 625 setTimeout(()=>{ 626 preloader.style.opacity='0'; 627 setTimeout(()=>{ 628 preloader.style.display='none'; 629 renderState(); 630 document.querySelector('.stage').style.opacity='1'; 631 setTimeout(checkAutoScroll,300); 632 },500); 633 },200); 634 }; 635 636 if(document.fonts && document.fonts.ready) { 637 document.fonts.ready.then(doHide); 638 } else { 639 doHide(); 640 } 641 setTimeout(doHide,1500); // Fallback 642} 643 644// âââââââââââââââââââââââââââââââââââââââââââââââ 645// TIMER & LIVE STATS 646// âââââââââââââââââââââââââââââââââââââââââââââââ 647function startLiveStats(){ 648 if(liveStatsInterval) clearInterval(liveStatsInterval); 649 liveStatsInterval = setInterval(()=>{ 650 if(!startTime||resultState!=='hidden')return; 651 const el=(new Date()-startTime)/60000; 652 const wpm=el>0?Math.round((charCount/5)/el):0; 653 const acc=charCount>0?Math.max(0,Math.min(100,Math.round(((charCount-errorsCount)/charCount)*100))):100; 654 document.getElementById('live-wpm').innerText=wpm; 655 document.getElementById('live-acc').innerText=acc+'%'; 656 },500); 657} 658 659function gameLoop() { 660 timeRemaining--; 661 updateTimerDisplay(); 662 updateProgressBar(); 663 if (timeRemaining <= 0) { 664 finishLevel(); 665 } 666} 667 668function updateTimerDisplay() { 669 const m = Math.floor(timeRemaining / 60).toString().padStart(2,'0'); 670 const s = (timeRemaining % 60).toString().padStart(2,'0'); 671 document.getElementById('live-timer').innerText = `${m}:${s}`; 672} 673 674function updateProgressBar(){ 675 const b = document.getElementById('typing-progress-bar'); 676 if(selectedSeconds > 0) { 677 const percentage = 100 - ((timeRemaining / selectedSeconds) * 100); 678 b.style.width = percentage + '%'; 679 } 680} 681
682function renderState(){ 683 document.querySelectorAll('.char').forEach((span,i)=>{ 684 span.className='char'; 685 const s=targetStacks[i]; 686 if(s.type==='enter'){span.classList.add('return-char');} 687 else{ 688 if(s.text.trim()==='à¼'||s.text.trim()==='à¼')span.classList.add('tsek'); 689 if(s.text.includes('à¼'))span.classList.add('sentence-break'); 690 if(s.text === ' ')span.classList.add('space-char'); 691 } 692 if(i<currentStackIndex)span.classList.add('correct'); 693 if(i===currentStackIndex){ 694 span.classList.add('current'); 695 if(s.type==='char'){ 696 document.getElementById('current-ghost')?.remove(); 697 const g=document.createElement('span');g.className='ghost-text';g.id='current-ghost';g.innerText=currentTyped;span.appendChild(g); 698 } 699 } 700 }); 701 updateGuide(); 702} 703 704// âââââââââââââââââââââââââââââââââââââââââââââââ 705// FIXED UPDATE GUIDE (MULTI-CHAR & MISSING SVGS) 706// âââââââââââââââââââââââââââââââââââââââââââââââ 707function updateGuide(){ 708 document.querySelectorAll('.key-guide').forEach(el=>el.classList.remove('key-guide')); 709 handContainer.innerHTML=''; 710 if(currentStackIndex>=targetStacks.length)return; 711 712 const targetObj=targetStacks[currentStackIndex]; 713 if(targetObj.type==='enter'){ 714 handContainer.innerHTML = svgCache[poseMap['enter']] || svgCache[poseMap['default']] || ''; 715 highlightSVGKey('ENTER','key-guide'); 716 return; 717 } 718 719 const targetStack=targetObj.text; 720 if(!targetStack.startsWith(currentTyped))return; 721 722 const remaining = targetStack.slice(currentTyped.length); 723 if(!remaining)return; 724 725 const DIRECT=new Set(['ྱ','ྲ','à¾','ཱ','à¼','à¼','à¾','ཿ','ཾ','à¾','ཱà¾']); 726 let key='', shift=false, requiresA=false, matchedVal=''; 727 728 const findKey = (map, isShift) => { 729 for(let [k, v] of Object.entries(map)) { 730 if(remaining.startsWith(v)) { 731 if (v.length > matchedVal.length) { 732 key = k; 733 shift = isShift; 734 matchedVal = v; 735 } 736 } 737 } 738 }; 739 740 if(isStackingMode){ 741 findKey(SUBJOINED_MAP, false); 742 findKey(SUBJOINED_SHIFT_MAP, true); 743 if(!key) findKey(NORMAL_MAP, false); 744 if(!key) findKey(SHIFT_MAP, true); 745 } else if(DIRECT.has(remaining[0])){ 746 findKey(NORMAL_MAP, false); 747 findKey(SHIFT_MAP, true); 748 } else { 749 findKey(NORMAL_MAP, false); 750 findKey(SHIFT_MAP, true); 751 if(!key) requiresA = true; 752 } 753 754 if(requiresA&&!isStackingMode){key='a';shift=false;} 755 else if(requiresA&&isStackingMode){ 756 findKey(SUBJOINED_MAP, false); 757 if(!key) findKey(SUBJOINED_SHIFT_MAP, true); 758 } 759 760 let poseFile=poseMap[matchedVal || remaining[0]]; 761 if(key==='a')poseFile=poseMap['a']; 762 763 if(key&&!poseFile){ 764 if(isStackingMode&&SUBJOINED_MAP[key]) poseFile=poseMap[SUBJOINED_MAP[key]]; 765 else if(NORMAL_MAP[key]) poseFile=poseMap[NORMAL_MAP[key]]; 766 } 767 768 if(key) { 769 if (poseFile && svgCache[poseFile]) { 770 handContainer.innerHTML = svgCache[poseFile]; 771 } else { 772 // No dedicated finger SVG (e.g. number/symbol keys like Shift+2) â show neutral rest pose 773 handContainer.innerHTML = svgCache[poseMap['default']] || ''; 774 } 775 highlightSVGKey(key,'key-guide'); 776 if(shift) highlightShiftKey(LEFT_HAND_KEYS.includes(key)?'right':'left','key-guide'); 777 } else { 778 handContainer.innerHTML = svgCache[poseMap['default']] || ''; 779 } 780} 781 782function highlightSVGKey(k,cls){ 783 const t=(k===' ')?'SPACEBAR':k.toUpperCase(); 784 document.querySelectorAll('text.st0').forEach(txt=>{if(txt.textContent.trim()===t)txt.parentElement.querySelector('rect.st1')?.classList.add(cls);}); 785} 786function highlightShiftKey(side,cls){ 787 const shifts=Array.from(document.querySelectorAll('text.st0')).filter(t=>t.textContent.trim()==='SHIFT'); 788 if(shifts.length>=2)(side==='left'?shifts[0]:shifts[1]).parentElement.querySelector('rect.st1')?.classList.add(cls); 789} 790 791function checkAutoScroll(){ 792 const active = document.querySelector('.char.current'); 793 if(active) { active.scrollIntoView({ behavior: 'smooth', block: 'center' }); } 794} 795 796// âââââââââââââââââââââââââââââââââââââââââââââââ 797// KEYDOWN 798// âââââââââââââââââââââââââââââââââââââââââââââââ 799window.addEventListener('keydown',e=>{ 800 if (document.getElementById('setup-overlay').style.display !== 'none') return; 801 if (resultState !== 'hidden') { 802 if(e.key==='Shift'||e.code==='ShiftLeft'||e.code==='ShiftRight') window.location.reload(); 803 if(e.code==='Space') window.location.href='index.html'; 804 return; 805 } 806 if (e.ctrlKey||e.metaKey||e.key==='Tab'||e.key==='Enter') { e.preventDefault(); return; } 807 808 // Start Timer on FIRST keystroke 809 if (!gameStarted && (e.key !== 'Shift' && e.key !== 'Alt' && e.key !== 'Control')) { 810 gameStarted = true; 811 document.querySelector('.phase-indicator').innerText = "Session Active"; 812 startTime = new Date(); 813 startLiveStats(); 814 timerInterval = setInterval(gameLoop, 1000); 815 } 816 817 // BACKSPACE 818 if(e.key==='Backspace'){ 819 e.preventDefault();sfx.init(); 820 if(isStackingMode){isStackingMode=false;updateGuide();return;} 821 if(currentTyped.length>0){ 822 currentTyped=currentTyped.slice(0,-1); 823 const span=document.getElementById(`stack-${currentStackIndex}`); 824 const ghost=document.getElementById('current-ghost'); 825 if(ghost){ghost.innerText=currentTyped;span?.classList.remove('wrong');ghost.classList.remove('error');updateGuide();} 826 } 827 return; 828 } 829 830 const targetStack=targetStacks[currentStackIndex].text; 831 if(!targetStack.startsWith(currentTyped)){sfx.init();sfx.playError();triggerErrorGlitch();return;} 832 833 // 'a' STACKING GATE 834 if(e.key==='a'&&!e.shiftKey&&!isStackingMode){ 835 e.preventDefault();sfx.init(); 836 const nextChar=targetStack[currentTyped.length]; 837 if(!shouldEnterStacking(nextChar,currentTyped)){ 838 errorsCount++;sfx.playError();triggerErrorGlitch(); 839 const span=document.getElementById(`stack-${currentStackIndex}`); 840 span?.classList.add('wrong');setTimeout(()=>span?.classList.remove('wrong'),200);
841 const ghost=document.getElementById('current-ghost'); 842 if(ghost){ghost.classList.add('error');if(errorTimeout)clearTimeout(errorTimeout); 843 errorTimeout=setTimeout(()=>{if(ghost&&ghost.isConnected)ghost.classList.remove('error');},300);} 844 return; 845 } 846 isStackingMode=true;sfx.playTick();updateGuide();return; 847 } 848 849 // REGULAR KEY 850 const unshift={'~':'`','!':'1','@':'2','#':'3','$':'4','%':'5','^':'6','&':'7','*':'8','(':'9',')':'0','_':'-','+':'=','{':'[','}':']','|':'\\',':':';','"':"'",'<':',','>':'.','?':'/'}; 851 let pk=e.key.toLowerCase(); 852 if(e.shiftKey&&unshift[e.key])pk=unshift[e.key]; 853 const attemptedStack=isStackingMode; 854 let typed=''; 855 if(isStackingMode){typed=e.shiftKey?SUBJOINED_SHIFT_MAP[pk]:SUBJOINED_MAP[pk];} 856 else{typed=e.shiftKey?SHIFT_MAP[pk]:NORMAL_MAP[pk];} 857 if(e.code==='Space'){typed=e.shiftKey?' ':'à¼';} 858 if(!typed)return; 859 860 e.preventDefault();sfx.init(); 861 highlightSVGKey(pk===' '?'SPACEBAR':pk,'key-hit'); 862 setTimeout(()=>document.querySelectorAll('.key-hit').forEach(el=>el.classList.remove('key-hit')),100); 863 864 if(currentStackIndex>=targetStacks.length) return; 865 866 const prospective=currentTyped+typed; 867 if(targetStack.startsWith(prospective)){ 868 if(attemptedStack)isStackingMode=false; 869 sfx.playClick();currentTyped=prospective;charCount++; 870 const ghost=document.getElementById('current-ghost'); 871 if(ghost){ghost.innerText=currentTyped;ghost.classList.remove('error');if(errorTimeout){clearTimeout(errorTimeout);errorTimeout=null;}} 872 873 if(currentTyped===targetStack){ 874 const cur=document.getElementById(`stack-${currentStackIndex}`); 875 cur?.classList.remove('current','wrong');cur?.classList.add('correct');ghost?.remove(); 876 currentStackIndex++;currentTyped=''; 877 878 // IF we run out of text, append more! 879 if (currentStackIndex >= targetStacks.length - 1) { 880 appendMoreText(); 881 } 882 883 if(currentStackIndex<targetStacks.length){ 884 const next=document.getElementById(`stack-${currentStackIndex}`); 885 next?.classList.add('current'); 886 document.getElementById('current-ghost')?.remove(); 887 const ng=document.createElement('span');ng.className='ghost-text';ng.id='current-ghost';next?.appendChild(ng); 888 updateGuide();checkAutoScroll(); 889 } 890 }else{updateGuide();} 891 }else{ 892 errorsCount++;sfx.playError();triggerErrorGlitch(); 893 const span=document.getElementById(`stack-${currentStackIndex}`); 894 span?.classList.add('wrong');setTimeout(()=>span?.classList.remove('wrong'),200); 895 const ghost=document.getElementById('current-ghost'); 896 if(ghost){ghost.innerText=prospective;ghost.classList.add('error'); 897 if(errorTimeout)clearTimeout(errorTimeout); 898 errorTimeout=setTimeout(()=>{if(ghost&&ghost.isConnected){ghost.classList.remove('error');ghost.innerText=currentTyped;}},300);} 899 } 900}); 901 902// âââââââââââââââââââââââââââââââââââââââââââââââ 903// RESULT SYSTEM 904// âââââââââââââââââââââââââââââââââââââââââââââââ 905const resultParticles={ 906 canvas:null,ctx:null,particles:[],active:false,W:0,H:0, 907 GC:['#FFD700','#00FFFF','#00E676','#FF6B9D','#C471ED','#74B9FF','#FF9F1C','#FFB7C5','#FFFFFF','#d4af37'], 908 GL:['à½','à½','à½','à½','འ','à½','à½','à½','à½','à½','à½','à½','à½','à½','à½','à½','ར','ལ','ས','ཨ','à¼','à¼'], 909 rg(){return this.GL[Math.floor(Math.random()*this.GL.length)];}, 910 rc(){return this.GC[Math.floor(Math.random()*this.GC.length)];}, 911 init(){this.canvas=document.getElementById('result-canvas');if(!this.canvas)return;this.W=this.canvas.width=window.innerWidth;this.H=this.canvas.height=window.innerHeight;this.ctx=this.canvas.getContext('2d');}, 912 statBurst(x,y,color){for(let i=0;i<12;i++){const a=Math.random()*Math.PI*2,sp=1.5+Math.random()*7;this.particles.push({type:'burst',x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-1,gravity:.1,alpha:1,decay:.024+Math.random()*.016,size:10+Math.random()*16,color,char:this.rg(),rot:Math.random()*Math.PI*2,rotSpd:(Math.random()-.5)*.15});}if(!this.active){this.active=true;this._loop();}}, 913 _loop(){ 914 if(!this.active||!this.ctx)return; 915 const{canvas,ctx,W,H}=this; ctx.clearRect(0,0,W,H); 916 this.particles=this.particles.filter(p=>p.alpha>.015); 917 for(const p of this.particles){ 918 if(p.type==='burst'){p.x+=p.vx;
918p.y+=p.vy;p.vy+=p.gravity;p.vx*=.97;p.alpha-=p.decay;p.rot+=p.rotSpd;ctx.save();ctx.globalAlpha=Math.max(0,p.alpha);ctx.font=`${p.size}px OuChan2,serif`;ctx.fillStyle=p.color;ctx.textAlign='center';ctx.textBaseline='middle';ctx.shadowColor=p.color;ctx.shadowBlur=14;ctx.translate(p.x,p.y);ctx.rotate(p.rot);ctx.fillText(p.char,0,0);ctx.restore();} 919 } 920 if(this.particles.length>0)requestAnimationFrame(()=>this._loop());else{this.active=false;ctx.clearRect(0,0,W,H);} 921 } 922}; 923 924function animateCounter(el,target,delay=0,onDone){ 925 setTimeout(()=>{ 926 el.classList.add('boom');setTimeout(()=>el.classList.remove('boom'),700); 927 if(typeof target==='string'){el.textContent=target;if(onDone)onDone();return;} 928 const dur=900,start=performance.now(); 929 function step(now){const t=Math.min((now-start)/dur,1);const ease=1-Math.pow(1-t,4);el.textContent=Math.round(ease*target);if(t<1)requestAnimationFrame(step);else{el.textContent=target;if(onDone)onDone();}} 930 requestAnimationFrame(step); 931 },delay); 932} 933 934function finishLevel(){ 935 if(timerInterval) clearInterval(timerInterval); 936 if(liveStatsInterval) clearInterval(liveStatsInterval); 937 938 // Exact mathematical output. We use selectedSeconds here. 939 const minutes = selectedSeconds / 60; 940 const wpm = minutes > 0 ? Math.round((charCount/5)/minutes) : 0; 941 const acc = charCount > 0 ? Math.max(0, Math.min(100, Math.round(((charCount-errorsCount)/charCount)*100))) : 0; 942 943 const mF = Math.floor(selectedSeconds / 60).toString().padStart(2,'0'); 944 const sF = (selectedSeconds % 60).toString().padStart(2,'0'); 945 document.getElementById('res-duration').innerText = `${mF}:${sF}`; 946 947 document.querySelectorAll('.res-stat').forEach(s=>s.classList.remove('visible')); 948 document.getElementById('res-wpm').innerText=0; 949 document.getElementById('res-acc').innerText=0; 950 951 document.getElementById('result-modal').style.display='flex'; 952 resultState='animating'; 953 954 resultParticles.init(); 955 956 const we=document.getElementById('res-wpm'),ae=document.getElementById('res-acc'); 957 const st=document.querySelectorAll('.res-stat'); 958 setTimeout(()=>{st[0]?.classList.add('visible');animateCounter(we,wpm,0,()=>{const r=we.getBoundingClientRect();resultParticles.statBurst(r.left+r.width/2,r.top+r.height/2,'#00FFFF');});},500); 959 setTimeout(()=>{st[1]?.classList.add('visible');animateCounter(ae,acc,0,()=>{const r=ae.getBoundingClientRect();resultParticles.statBurst(r.left+r.width/2,r.top+r.height/2,'#00E676');});},900); 960 setTimeout(()=>{resultState='finished';}, 1500); 961} 962 963// âââââââââââââââââââââââââââââââââââââââââââââââ 964// INITIAL LOAD 965// âââââââââââââââââââââââââââââââââââââââââââââââ 966window.addEventListener('load',()=>{ 967 const savedLayout = localStorage.getItem('tk_pref_layout_paragraph'); 968 setLayout(savedLayout || 'layout-small', false); 969 970 if(localStorage.getItem(PREF_HANDS)==='true') toggleHands(false); 971 if(localStorage.getItem(PREF_SOUND)==='true'){ sfx.muted=true; document.getElementById('sound-btn').classList.add('muted'); } 972 973 const savedSize = localStorage.getItem(PREF_TEXT_SIZE); 974 if(savedSize){ currentFontSize = parseInt(savedSize); document.body.style.setProperty('--dynamic-font-size',currentFontSize+'px'); } 975}); 976</script>
976 977</body> 978</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.