1/** 2 * @file main.js 3 * @description Entry point -- photo overlay construction, simulation start sequence, 4 * share functionality, and DOM-ready event binding. 5 * Loaded last; wires up all UI buttons and kicks off the scene chain. 6 * @requires i18n.js - t() for all UI text, applyI18n() for initial DOM translation 7 * @requires audio.js - initAudio(), simPaused, simTimers, togglePause(), bgMusic 8 * @requires helpers.js - mkPhoto-internal helpers (setLayer), toast() 9 * @requires timer.js - sec, tmr, tick(), startClock() 10 * @requires firebase-counter.js - incrementCounters() (optional, checked with typeof) 11 * @requires scenes/p1-whatsapp.js - p1() scene entry 12 */ 13 14// ========== PHOTO OVERLAY ========== 15 16/** 17 * Builds a 3-layer photo overlay inside the given element. 18 * The layers represent progressive defacement of Tom's photo: 19 * - Layer 1 (real-photo): the original unedited photo (always visible) 20 * - Layer 2 (.e2): Instagram-style edits -- mean emojis, hurtful text overlays 21 * - Layer 3 (.e3 + .igfr + .grain): TikTok-style additions -- meme captions, 22 * Instagram frame, and film grain filter 23 * Layer visibility is controlled separately by setLayer(). 24 * @param {HTMLElement} el - Container element to populate 25 * @param {string} [h='100%'] - CSS height for the container 26 */ 27function mkPhoto(el, h) { 28 el.style.height = h || '100%'; 29 el.innerHTML = 30 '<div class="photo-wrap">' + 31 '<div class="real-photo"></div>' + 32 '<div class="edits">' + 33 // Layer 2: Instagram - typische Insta-Story Bearbeitungen 34 '<div class="e e2 e2-emoji-top">' + 35 t('photo.emoji1') + 36 '</div>' + 37 '<div class="e e2 e2-text-look">' + 38 t('photo.lookText') + 39 '</div>' + 40 '<div class="e e2 e2-poop">' + 41 t('photo.poop') + 42 '</div>' + 43 '<div class="e e2 e2-ugly">' + 44 t('photo.ugly') + 45 '</div>' + 46 '<div class="e e2 e2-never">' + 47 t('photo.never') + 48 '</div>' + 49 // Layer 3: TikTok - Meme-Style, TikTok-typische Texte 50 '<div class="e e3 e3-exposed">' + 51 t('photo.exposed') + 52 '</div>' + 53 '<div class="e e3 e3-pov">' + 54 t('photo.pov') + 55 '</div>' + 56 '<div class="e e3 e3-tags">' + 57 t('photo.tags') + 58 '</div>' + 59 '<div class="e e3 e3-emoji-bot">' + 60 t('photo.emojiBot') + 61 '</div>' + 62 '<div class="e e3 e3-bozo">' + 63 t('photo.bozo') + 64 '</div>' + 65 '</div>' + 66 '<div class="igfr" data-user="' + 67 t('photo.igUser') + 68 '" data-likes="' + 69 t('photo.igLikes') + 70 '"></div>' + 71 '<div class="grain"></div>' + 72 '</div>'; 73} 74 75// ========== START ========== 76 77/** 78 * Simulation initialization sequence. Called when the user clicks the start button. 79 * Resets pause state and timers, increments the Firebase view counter (if available), 80 * hides the start screen, shows the phone UI, builds the photo overlays for 81 * Instagram and TikTok scenes, then after a brief delay starts the progress bar, 82 * phone clock, and the first scene (p1 WhatsApp). 83 */ 84var simStarted = false; 85function go() { 86 if (simStarted) return; // Guard against double-click 87 simStarted = true; 88 initAudio(); 89 simPaused = false; 90 simTimers = []; 91 if (typeof incrementCounters === 'function') incrementCounters(); 92 document.getElementById('start').classList.add('gone'); 93 var disc = document.querySelector('.disclaimer'); 94 if (disc) disc.classList.add('hidden'); 95 document.getElementById('pauseBtn').classList.remove('hidden'); 96 document.getElementById('pauseBtn').textContent = t('ui.pause'); 97 document.getElementById('pauseOverlay').classList.add('hidden'); 98 document.getElementById('phone').classList.remove('hidden'); 99 mkPhoto(document.getElementById('igPh')); 100 mkPhoto(document.getElementById('tkBg')); 101 setLayer(1); 102 initClock(); 103 // simTimeout (not native setTimeout) so the start delay is pausable and 104 // clearable via simTimers like every other scheduled scene step 105 simTimeout(function () { 106 sec = 0; 107 tmr = setInterval(tick, 100 / SIM_SPEED); 108 startClock(); 109 p1(); 110 }, 500); 111} 112 113// ========== SHARE ========== 114 115/** 116 * Shares the simulation URL. On mobile devices with Web Share API support, 117 * opens the native share sheet. On desktop, copies the URL to the clipboard 118 * (with a textarea fallback for HTTP contexts where navigator.clipboard is unavailable) 119 * and shows a confirmation toast. 120 */ 121function shareSimulation() { 122 var url = window.location.href; 123 var isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); 124 125 if (isMobile && navigator.share) { 126 navigator 127 .share({ 128 title: t('share.title'), 129 text: t('share.text'), 130 url: url, 131 }) 132 .catch(function () {}); 133 return; 134 } 135 136 // Desktop: Clipboard API with textarea fallback for HTTP 137 if (navigator.clipboard && navigator.clipboard.writeText) { 138 navigator.clipboard.writeText(url).catch(function () {}); 139 } else { 140 var ta = document.createElement('textarea'); 141 ta.value = url; 142 ta.className = 'sr-only-input'; 143 document.body.appendChild(ta); 144 ta.focus(); 145 ta.select(); 146 try { 147 document.execCommand('copy'); 148 } catch (e) {} 149 document.body.removeChild(ta); 150 } 151 152 // Show confirmation 153 var toastEl = document.getElementById('toast'); 154 toastEl.classList.remove('hidden'); 155 toastEl.textContent = t('ui.linkCopied'); 156 toastEl.classList.add('show'); 157 simTimeout(function () { 158 toastEl.classList.remove('show'); 159 }, 2500); 160} 161 162// ========== DOM READY ========== 163/** 164 * DOMContentLoaded handler -- wires up all interactive elements: 165 * - Start button -> go() 166 * - Share buttons -> shareSimulation() 167 * - Replay button -> page reload 168 * - Pause button + overlay -> togglePause() 169 * - Impressum modal (open/close/backdrop/Escape) 170 * - Applies i18n translations to the initial DOM 171 */ 172document.addEventListener('DOMContentLoaded', function () { 173 bgMusic = document.getElementById('bgm'); 174 var impModal = document.getElementById('impModal'); 175 applyI18n(); 176 // Keep the brand suffix from the static SEO title (index.html) when 177 // swapping in the localized runtime title (BIZ-01) 178 document.title = t('ui.title') + ' \u2013 ' + t('ui.subtitle') + ' | malziland'; 179 if (impModal) impModal.setAttribute('aria-label', t('imp.title')); 180 181 // Append helpline logo disclaimer if a logo is configured 182 var cfg = typeof helplineConfig !== 'undefined' ? helplineConfig : {}; 183 if (cfg.logo) { 184 var discEl = document.querySelector('.disclaimer'); 185 if (discEl) discEl.textContent += ' ' + t('disclaimer.helplineLogo'); 186 } 187 188 function openImpressum() { 189 impModal.classList.add('show'); 190 } 191 function closeImpressum() { 192 impModal.classList.remove('show'); 193 } 194 195 // Start button 196 var startBtn = document.getElementById('startBtn'); 197 if (startBtn) { 198 startBtn.addEventListener('click', function () { 199 if (bgMusic) { 200 bgMusic.loop = true; 201 bgMusic.volume = 0.4; 202 bgMusic.play().catch(function () {}); 203 } 204 go(); 205 }); 206 } 207 208 // Share buttons 209 var startShareBtn = document.getElementById('startShareBtn'); 210 var footerShareBtn = document.getElementById('footerShareBtn'); 211 if (startShareBtn) startShareBtn.addEventListener('click', shareSimulation); 212 if (footerShareBtn) footerShareBtn.addEventListener('click', shareSimulation); 213 214 // Replay button 215 var replayBtn = document.getElementById('footerReplayBtn'); 216 if (replayBtn) 217 replayBtn.addEventListener('click', function () { 218 window.location.reload(); 219 }); 220 221 // Pause 222 var pauseBtn = document.getElementById('pauseBtn'); 223 var pauseOverlay = document.getElementById('pauseOverlay'); 224 if (pauseBtn) pauseBtn.addEventListener('click', togglePause); 225 if (pauseOverlay) pauseOverlay.addEventListener('click', togglePause); 226 227 // Impressum links 228 var impLinkGlobal = document.getElementById('impLinkGlobal'); 229 var impCloseBtn = document.getElementById('impCloseBtn'); 230 if (impLinkGlobal) { 231 impLinkGlobal.addEventListener('click', openImpressum); 232 // span[role=button] gets no synthetic click on Enter/Space like a real 233 // <button> does -- required for keyboard operability (WCAG 2.1.1) 234 impLinkGlobal.addEventListener('keydown', function (e) { 235 if (e.key === 'Enter' || e.key === ' ') { 236 e.preventDefault(); 237 openImpressum(); 238 } 239 }); 240 } 241 if (impCloseBtn) impCloseBtn.addEventListener('click', closeImpressum); 242 243 // Impressum modal: close on backdrop click and Escape key 244 if (impModal) { 245 impModal.addEventListener('click', function (e) { 246 if (e.target === impModal) closeImpressum(); 247 }); 248 document.addEventListener('keydown', function (e) { 249 if (e.key === 'Escape' && impModal.classList.contains('show')) closeImpressum(); 250 }); 251 } 252});
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.