PageSourceSearch

https://cybermobbing.web.app/js/main.js?v=1784223914

js cybermobbing.web.app collected 2026-10-03 12:20:50 UTC 8,746 bytes, 252 lines download raw bytes

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.