PageSourceSearch

https://zvukipro.com/templates/moby/js/tb-waveform.js?v=8.2

js zvukipro.com collected 2026-10-01 10:44:55 UTC 22,233 bytes, 730 lines download raw bytes

1/* tb-waveform.js (v8) — DLE-friendly, no WebAudio, modern overlay waveform
2   Fixes:
3   - “две волны друг под другом” (если CSS не применился) → инжектим базовый CSS
4   - overlay canvas рисуется по ширине бокса (а не по ширине overlayWrap)
5   - фиксируем высоту (CFG.height)
6*/
7(function () {
8  'use strict';
9
10  var CFG = {
11    endpoint: '/tb_wave.php',
12    height: 54,
13    rootMargin: '600px',
14    maxConcurrency: 2,
15    localCacheTTL: 7 * 24 * 3600, // 7 дней
16    selectorPlayer: '.dleaudioplayer',
17    debug: false,
18    injectCSS: true
19  };
20
21  function log() {
22    if (!CFG.debug) return;
23    try { console.log.apply(console, arguments); } catch (_) {}
24  }
25
26  function onReady(fn) {
27    if (document.readyState === 'loading') {
28      document.addEventListener('DOMContentLoaded', fn, { once: true });
29    } else {
30      fn();
31    }
32  }
33
34  function clamp(n, a, b) { return Math.max(a, Math.min(b, n)); }
35
36  function debounce(fn, ms) {
37    var t = null;
38    return function () {
39      clearTimeout(t);
40      var args = arguments;
41      t = setTimeout(function () { fn.apply(null, args); }, ms);
42    };
43  }
44
45  function hashDJB2(str) {
46    var h = 5381, i = str.length;
47    while (i) h = (h * 33) ^ str.charCodeAt(--i);
48    return (h >>> 0).toString(16);
49  }
50
51  function nowSec() { return Math.floor(Date.now() / 1000); }
52
53  function getLocal(key) {
54    try {
55      var raw = localStorage.getItem(key);
56      if (!raw) return null;
57      var obj = JSON.parse(raw);
58      if (!obj || !obj.t || !obj.v) return null;
59      if (nowSec() - obj.t > CFG.localCacheTTL) return null;
60      return obj.v;
61    } catch (_) {
62      return null;
63    }
64  }
65
66  function setLocal(key, value) {
67    try {
68      localStorage.setItem(key, JSON.stringify({ t: nowSec(), v: value }));
69    } catch (_) {}
70  }
71
72  // --- базовый CSS (чтобы не было “двух волн” при слетевших стилях) ---
73  function injectStyles() {
74    if (!CFG.injectCSS) return;
75    if (document.getElementById('tb-waveform-style')) return;
76
77    var css =
78      '.tb-wave{--tb-wave-h:' + (CFG.height|0) + 'px;--tb-wave-fill:rgba(59,130,246,.35);--tb-wave-line:rgba(59,130,246,.90);' +
79      '--tb-wave-base-fill:rgba(15,23,42,.10);--tb-wave-base-line:rgba(15,23,42,.25);--tb-wave-grid:rgba(15,23,42,.10);' +
80      'display:block;margin:10px 0 8px;}' +
81
82      '.tb-wave__box{position:relative;height:var(--tb-wave-h);border-radius:12px;overflow:hidden;' +
83      'background:rgba(15,23,42,.03);border:1px solid rgba(15,23,42,.08);}' +
84
85      '.tb-wave__canvas{display:block;position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;}' +
86
87      '.tb-wave__overlayWrap{position:absolute;left:0;top:0;height:100%;width:100%;overflow:hidden;}' +
88
89      '.tb-wave__tip{position:absolute;top:6px;transform:translateX(-50%);' +
90      'padding:2px 6px;border-radius:8px;font-size:11px;line-height:1.3;' +
91      'background:rgba(15,23,42,.85);color:#fff;white-space:nowrap;opacity:0;pointer-events:none;}' +
92      '.tb-wave__tip.is-on{opacity:1;}' +
93
94      '.tb-wave__status{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;' +
95      'font-size:12px;color:rgba(15,23,42,.55);pointer-events:none;}' +
96
97      '.tb-wave.is-loading .tb-wave__box:before{content:"";position:absolute;inset:0;' +
98      'background:linear-gradient(90deg,rgba(255,255,255,0) 0%,rgba(255,255,255,.35) 50%,rgba(255,255,255,0) 100%);' +
99      'transform:translateX(-100%);animation:tbwave_shimmer 1.2s infinite;}' +
100      '@keyframes tbwave_shimmer{to{transform:translateX(100%);}}' +
101
102      '.tb-wave.is-error .tb-wave__box{border-color:rgba(239,68,68,.35);background:rgba(239,68,68,.06);}' +
103      '.tb-wave__box{cursor:pointer;}' +
104      '.tb-wave__box:hover{border-color:rgba(59,130,246,.30);box-shadow:0 6px 18px rgba(2,6,23,.08);}';
105
106    var style = document.createElement('style');
107    style.id = 'tb-waveform-style';
108    style.type = 'text/css';
109    style.appendChild(document.createTextNode(css));
110    document.head.appendChild(style);
111  }
112
113  // --- request queue (чтобы не словить 429 на пачке волн) ---
114  var q = [];
115  var active = 0;
116
117  function enqueue(taskFn) {
118    q.push(taskFn);
119    pump();
120  }
121
122  function pump() {
123    while (active < CFG.maxConcurrency && q.length) {
124      active++;
125      var fn = q.shift();
126      Promise.resolve()
127        .then(fn)
128        .catch(function () {})
129        .finally(function () {
130          active--;
131          pump();
132        });
133    }
134  }
135
136  // --- UI builders ---
137  function createWaveShell() {
138    var wrap = document.createElement('div');
139    wrap.className = 'tb-wave is-loading';
140    wrap.style.setProperty('--tb-wave-h', (CFG.height|0) + 'px');
141    wrap.innerHTML =
142      '<div class="tb-wave__box">' +
143        '<canvas class="tb-wave__canvas tb-wave__base"></canvas>' +
144        '<div class="tb-wave__overlayWrap">' +
145          '<canvas class="tb-wave__canvas tb-wave__overlay"></canvas>' +
146        '</div>' +
147        '<div class="tb-wave__tip" aria-hidden="true"></div>' +
148        '<div class="tb-wave__status" aria-hidden="true"></div>' +
149      '</div>';
150    // на всякий — если CSS не успел примениться
151    var box = wrap.querySelector('.tb-wave__box');
152    if (box) box.style.height = (CFG.height|0) + 'px';
153    return wrap;
154  }
155
156  function setStatus(waveEl, text) {
157    var s = waveEl.querySelector('.tb-wave__status');
158    if (!s) return;
159    s.textContent = text || '';
160  }
161
162  function setError(waveEl, text) {
163    waveEl.classList.remove('is-loading');
164    waveEl.classList.add('is-error');
165    setStatus(waveEl, text || 'ошибка');
166  }
167
168  function setReady(waveEl) {
169    waveEl.classList.remove('is-loading');
170    waveEl.classList.remove('is-error');
171    setStatus(waveEl, '');
172  }
173
174  // --- data ---
175  function normalizeSrc(src) {
176    try {
177      var u = new URL(src, location.href);
178      if (u.origin === location.origin) return u.pathname + (u.search || '');
179      return u.href;
180    } catch (_) {
181      return src;
182    }
183  }
184
185  function buildWaveUrl(src) {
186    var s = normalizeSrc(src);
187    return CFG.endpoint + '?src=' + encodeURIComponent(s);
188  }
189
190  function parseWaveJson(obj) {
191    if (!obj || !Array.isArray(obj.peaks) || !obj.peaks.length) return null;
192
193    var maxV = 0;
194    for (var i = 0; i < obj.peaks.length; i++) {
195      var v = obj.peaks[i] | 0;
196      if (v > maxV) maxV = v;
197    }
198    if (maxV <= 0) maxV = 100;
199
200    return {
201      peaks: obj.peaks,
202      duration: typeof obj.d === 'number' ? obj.d : null,
203      maxV: maxV
204    };
205  }
206
207  function fetchWave(src) {
208    var cacheKey = 'tbwave:' + hashDJB2(src);
209    var cached = getLocal(cacheKey);
210    if (cached) {
211      var parsed = parseWaveJson(cached);
212      if (parsed) return Promise.resolve(parsed);
213    }
214
215    var url = buildWaveUrl(src);
216
217    return fetch(url, { credentials: 'same-origin' })
218      .then(function (r) {
219        if (!r.ok) throw new Error('HTTP ' + r.status);
220        return r.json();
221      })
222      .then(function (json) {
223        setLocal(cacheKey, json);
224        var parsed = parseWaveJson(json);
225        if (!parsed) throw new Error('bad json');
226        return parsed;
227      });
228  }
229
230  // --- drawing ---
231  function fitCanvas(canvas, cssW, cssH) {
232    var dpr = Math.max(1, Math.min(3, window.devicePixelRatio || 1));
233    canvas.style.width = cssW + 'px';
234    canvas.style.height = cssH + 'px';
235    canvas.width = Math.floor(cssW * dpr);
236    canvas.height = Math.floor(cssH * dpr);
237    var ctx = canvas.getContext('2d');
238    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
239    return ctx;
240  }
241
242  function getCssVar(el, name, fallback) {
243    try {
244      var v = getComputedStyle(el).getPropertyValue(name);
245      v = (v || '').trim();
246      return v || fallback;
247    } catch (_) {
248      return fallback;
249    }
250  }
251
252  function smoothPeaks(peaks, windowSize) {
253    windowSize = windowSize || 3;
254    if (windowSize <= 1) return peaks;
255    var out = new Array(peaks.length);
256    var half = Math.floor(windowSize / 2);
257    for (var i = 0; i < peaks.length; i++) {
258      var sum = 0, cnt = 0;
259      for (var j = i - half; j <= i + half; j++) {
260        if (j < 0 || j >= peaks.length) continue;
261        sum += (peaks[j] | 0);
262        cnt++;
263      }
264      out[i] = cnt ? Math.round(sum / cnt) : (peaks[i] | 0);
265    }
266    return out;
267  }
268
269  function getBoxSize(waveEl) {
270    var box = waveEl.querySelector('.tb-wave__box');
271    if (!box) return null;
272    var w = box.clientWidth || 0;
273    var h = box.clientHeight || 0;
274    if (!h) {
275      h = (CFG.height|0) || 54;
276      box.style.height = h + 'px';
277    }
278    return { box: box, w: w, h: h };
279  }
280
281  function drawWave(canvas, peaks, maxV, opts, waveEl) {
282    opts = opts || {};
283
284    var s = waveEl ? getBoxSize(waveEl) : null;
285    var cssW = s ? s.w : (canvas.parentElement ? canvas.parentElement.clientWidth : canvas.clientWidth);
286    var cssH = s ? s.h : (canvas.parentElement ? canvas.parentElement.clientHeight : canvas.clientHeight);
287
288    if (!cssW || !cssH) return;
289
290    var ctx = fitCanvas(canvas, cssW, cssH);
291    ctx.clearRect(0, 0, cssW, cssH);
292
293    var mid = Math.floor(cssH / 2);
294    var pad = 8;
295    var ampH = mid - pad;
296
297    var p = smoothPeaks(peaks, 3);
298    var n = p.length;
299    var w = cssW;
300
301    var styleRoot = waveEl ? waveEl : (canvas.parentElement || document.body);
302
303    var cFill = opts.fill || getCssVar(styleRoot, '--tb-wave-fill', 'rgba(59,130,246,.35)');
304    var cLine = opts.line || getCssVar(styleRoot, '--tb-wave-line', 'rgba(59,130,246,.85)');
305    var cGrid = opts.grid || getCssVar(styleRoot, '--tb-wave-grid', 'rgba(15,23,42,.10)');
306
307    // центральная линия
308    if (cGrid && cGrid !== 'rgba(0,0,0,0)') {
309      ctx.lineWidth = 1;
310      ctx.strokeStyle = cGrid;
311      ctx.beginPath();
312      ctx.moveTo(0, mid + 0.5);
313      ctx.lineTo(w, mid + 0.5);
314      ctx.stroke();
315    }
316
317    ctx.lineJoin = 'round';
318    ctx.lineCap = 'round';
319
320    ctx.beginPath();
321    ctx.moveTo(0, mid);
322
323    for (var x = 0; x <= w; x++) {
324      var idx = Math.floor((x / w) * (n - 1));
325      var a = (p[idx] | 0) / maxV;
326      a = clamp(a, 0, 1);
327      var y = mid - (a * ampH);
328      ctx.lineTo(x, y);
329    }
330
331    for (var x2 = w; x2 >= 0; x2--) {
332      var idx2 = Math.floor((x2 / w) * (n - 1));
333      var a2 = (p[idx2] | 0) / maxV;
334      a2 = clamp(a2, 0, 1);
335      var y2 = mid + (a2 * ampH);
336      ctx.lineTo(x2, y2);
337    }
338
339    ctx.closePath();
340    ctx.fillStyle = cFill;
341    ctx.fill();
342
343    // верхняя обводка
344    ctx.beginPath();
345    for (var x3 = 0; x3 <= w; x3++) {
346      var idx3 = Math.floor((x3 / w) * (n - 1));
347      var a3 = (p[idx3] | 0) / maxV;
348      a3 = clamp(a3, 0, 1);
349      var y3 = mid - (a3 * ampH);
350      if (x3 === 0) ctx.moveTo(x3, y3);
351      else ctx.lineTo(x3, y3);
352    }
353    ctx.strokeStyle = cLine;
354    ctx.lineWidth = 1.35;
355    ctx.stroke();
356  }
357
358  function setProgress(waveEl, ratio) {
359    ratio = clamp(ratio, 0, 1);
360    var o = waveEl.querySelector('.tb-wave__overlayWrap');
361    if (!o) return;
362    o.style.width = (ratio * 100).toFixed(4) + '%';
363  }
364
365  // --- seek / binding ---
366  function formatTime(sec) {
367    if (!isFinite(sec) || sec < 0) sec = 0;
368    var m = Math.floor(sec / 60);
369    var s = Math.floor(sec % 60);
370    return m + ':' + (s < 10 ? '0' + s : s);
371  }
372
373  function findAudioNear(playerEl, src) {
374    var a = playerEl.querySelector('audio');
375    if (a) return a;
376
377    var root = playerEl.closest('#dle-content') || document;
378    var audios = root.querySelectorAll('audio');
379    if (!audios.length) return null;
380
381    var needle = '';
382    try {
383      var u = new URL(src, location.href);
384      needle = (u.pathname || '').split('/').pop() || '';
385    } catch (_) {
386      needle = (src || '').split('/').pop() || '';
387    }
388
389    for (var i = 0; i < audios.length; i++) {
390      var cur = audios[i].currentSrc || audios[i].src || '';
391      if (needle && cur.indexOf(needle) !== -1) return audios[i];
392    }
393
394    if (audios.length === 1) return audios[0];
395    return null;
396  }
397
398  function waitForAudio(playerEl, src, timeoutMs) {
399    timeoutMs = timeoutMs || 2500;
400    return new Promise(function (resolve) {
401      var found = findAudioNear(playerEl, src);
402      if (found) return resolve(found);
403
404      var done = false;
405      var t = setTimeout(function () {
406        done = true;
407        try { obs.disconnect(); } catch (_) {}
408        resolve(null);
409      }, timeoutMs);
410
411      var obs = new MutationObserver(function () {
412        if (done) return;
413        var a = findAudioNear(playerEl, src);
414        if (a) {
415          done = true;
416          clearTimeout(t);
417          try { obs.disconnect(); } catch (_) {}
418          resolve(a);
419        }
420      });
421
422      obs.observe(playerEl.parentNode || document.body, { childList: true, subtree: true });
423    });
424  }
425
426  function bindSeek(waveEl, playerEl, src, durationHint) {
427    if (waveEl.dataset.tbSeekBound === '1') return;
428    waveEl.dataset.tbSeekBound = '1';
429
430    var box = waveEl.querySelector('.tb-wave__box') || waveEl;
431    var tip = waveEl.querySelector('.tb-wave__tip');
432    var isDown = false;
433    var lastRatio = 0;
434
435    function rectOf(el) { return el.getBoundingClientRect(); }
436
437    function ratioFromEvent(e) {
438      var r = rectOf(box);
439      var x = (e.clientX - r.left) / r.width;
440      return clamp(x, 0, 1);
441    }
442
443    function showTip(e, dur) {
444      if (!tip) return;
445      var r = rectOf(box);
446      var x = clamp(e.clientX - r.left, 0, r.width);
447      var ratio = x / r.width;
448      var sec = dur ? (ratio * dur) : 0;
449      tip.style.left = x + 'px';
450      tip.textContent = dur ? formatTime(sec) : '';
451      tip.classList.add('is-on');
452    }
453
454    function hideTip() {
455      if (!tip) return;
456      tip.classList.remove('is-on');
457    }
458
459    function getDuration(audio) {
460      var d = audio && isFinite(audio.duration) && audio.duration > 0 ? audio.duration : null;
461      if (!d && durationHint && durationHint > 0) d = durationHint;
462      return d;
463    }
464
465    function applySeek(ratio) {
466      lastRatio = ratio;
467
468      var audio = findAudioNear(playerEl, src);
469      if (!audio) {
470        waveEl.dataset.tbPendingSeek = String(ratio);
471        setProgress(waveEl, ratio);
472        return;
473      }
474
475      var dur = getDuration(audio);
476      if (!dur) {
477        waveEl.dataset.tbPendingSeek = String(ratio);
478        setProgress(waveEl, ratio);
479        return;
480      }
481
482      try {
483        audio.currentTime = ratio * dur;
484        setProgress(waveEl, ratio);
485      } catch (_) {}
486    }
487
488    box.addEventListener('pointerdown', function (e) {
489      isDown = true;
490      try { box.setPointerCapture(e.pointerId); } catch (_) {}
491      applySeek(ratioFromEvent(e));
492      e.preventDefault();
493    });
494
495    box.addEventListener('pointermove', function (e) {
496      var audio = findAudioNear(playerEl, src);
497      var dur = getDuration(audio);
498      showTip(e, dur);
499
500      if (!isDown) return;
501      applySeek(ratioFromEvent(e));
502      e.preventDefault();
503    });
504
505    box.addEventListener('pointerup', function (e) {
506      isDown = false;
507      hideTip();
508      e.preventDefault();
509    });
510
511    box.addEventListener('pointercancel', function () {
512      isDown = false;
513      hideTip();
514    });
515
516    box.addEventListener('mouseleave', function () {
517      if (!isDown) hideTip();
518    });
519
520    waitForAudio(playerEl, src, 3500).then(function (audio) {
521      if (!audio) return;
522
523      function syncProgress() {
524        var dur = getDuration(audio);
525        if (!dur) return;
526        setProgress(waveEl, audio.currentTime / dur);
527      }
528
529      audio.addEventListener('timeupdate', syncProgress);
530      audio.addEventListener('seeked', syncProgress);
531      audio.addEventListener('loadedmetadata', function () {
532        var p = waveEl.dataset.tbPendingSeek;
533        if (p) {
534          delete waveEl.dataset.tbPendingSeek;
535          applySeek(parseFloat(p) || lastRatio);
536        } else {
537          syncProgress();
538        }
539      });
540
541      syncProgress();
542    });
543  }
544
545  // --- attach workflow ---
546  var io = null;
547
548  function ensureIO() {
549    if (io) return;
550    if (!('IntersectionObserver' in window)) return;
551
552    io = new IntersectionObserver(function (entries) {
553      entries.forEach(function (ent) {
554        if (!ent.isIntersecting) return;
555        var waveEl = ent.target;
556        io.unobserve(waveEl);
557
558        var src = waveEl.dataset.tbSrc;
559        var playerId = waveEl.dataset.tbPlayerId;
560        var playerEl = playerId ? document.querySelector('[data-tb-player-id="' + playerId + '"]') : null;
561        if (!src || !playerEl) return;
562
563        enqueue(function () {
564          setStatus(waveEl, 'loading...');
565          return fetchWave(src)
566            .then(function (data) {
567              waveEl.dataset.tbDuration = data.duration ? String(data.duration) : '';
568
569              var base = waveEl.querySelector('canvas.tb-wave__base');
570              var over = waveEl.querySelector('canvas.tb-wave__overlay');
571              if (!base || !over) throw new Error('no canvas');
572
573              drawWave(base, data.peaks, data.maxV, {
574                fill: getCssVar(waveEl, '--tb-wave-base-fill', 'rgba(15,23,42,.10)'),
575                line: getCssVar(waveEl, '--tb-wave-base-line', 'rgba(15,23,42,.25)'),
576                grid: getCssVar(waveEl, '--tb-wave-grid', 'rgba(15,23,42,.10)')
577              }, waveEl);
578
579              drawWave(over, data.peaks, data.maxV, {
580                fill: getCssVar(waveEl, '--tb-wave-fill', 'rgba(59,130,246,.35)'),
581                line: getCssVar(waveEl, '--tb-wave-line', 'rgba(59,130,246,.90)'),
582                grid: 'rgba(0,0,0,0)'
583              }, waveEl);
584
585              setReady(waveEl);
586
587              var dHint = data.duration || parseFloat(waveEl.dataset.tbDuration || '0') || 0;
588              bindSeek(waveEl, playerEl, src, dHint);
589
590              setProgress(waveEl, 0);
591            })
592            .catch(function (err) {
593              log('wave error', err);
594              setError(waveEl, (err && err.message) ? err.message : 'ошибка');
595            });
596        });
597      });
598    }, { root: null, rootMargin: CFG.rootMargin, threshold: 0.02 });
599  }
600
601  function attachToPlayer(playerEl) {
602    if (!playerEl || playerEl.dataset.tbWaveAttached === '1') return;
603
604    var li = playerEl.querySelector('li[data-url]');
605    if (!li) return;
606
607    var src = li.getAttribute('data-url');
608    if (!src) return;
609
610    var prev = playerEl.previousElementSibling;
611    if (prev && prev.classList && prev.classList.contains('tb-wave')) {
612      playerEl.dataset.tbWaveAttached = '1';
613      return;
614    }
615
616    var pid = playerEl.dataset.tbPlayerId;
617    if (!pid) {
618      pid = 'p' + Math.random().toString(16).slice(2);
619      playerEl.dataset.tbPlayerId = pid;
620    }
621
622    var waveEl = createWaveShell();
623    waveEl.dataset.tbSrc = src;
624    waveEl.dataset.tbPlayerId = pid;
625
626    playerEl.parentNode.insertBefore(waveEl, playerEl);
627    playerEl.dataset.tbWaveAttached = '1';
628
629    ensureIO();
630    if (io) {
631      io.observe(waveEl);
632    } else {
633      enqueue(function () {
634        return fetchWave(src)
635          .then(function (data) {
636            var base = waveEl.querySelector('canvas.tb-wave__base');
637            var over = waveEl.querySelector('canvas.tb-wave__overlay');
638
639            drawWave(base, data.peaks, data.maxV, {
640              fill: getCssVar(waveEl, '--tb-wave-base-fill', 'rgba(15,23,42,.10)'),
641              line: getCssVar(waveEl, '--tb-wave-base-line', 'rgba(15,23,42,.25)'),
642              grid: getCssVar(waveEl, '--tb-wave-grid', 'rgba(15,23,42,.10)')
643            }, waveEl);
644
645            drawWave(over, data.peaks, data.maxV, {
646              fill: getCssVar(waveEl, '--tb-wave-fill', 'rgba(59,130,246,.35)'),
647              line: getCssVar(waveEl, '--tb-wave-line', 'rgba(59,130,246,.90)'),
648              grid: 'rgba(0,0,0,0)'
649            }, waveEl);
650
651            setReady(waveEl);
652
653            var dHint = data.duration || 0;
654            bindSeek(waveEl, playerEl, src, dHint);
655            setProgress(waveEl, 0);
656          })
657          .catch(function (err) {
658            setError(waveEl, (err && err.message) ? err.message : 'ошибка');
659          });
660      });
661    }
662  }
663
664  function scan() {
665    var players = document.querySelectorAll(CFG.selectorPlayer);
666    for (var i = 0; i < players.length; i++) {
667      attachToPlayer(players[i]);
668    }
669  }
670
671  function resizeAll() {
672    var waves = document.querySelectorAll('.tb-wave');
673    for (var i = 0; i < waves.length; i++) {
674      var waveEl = waves[i];
675      var src = waveEl.dataset.tbSrc;
676      if (!src) continue;
677
678      var cacheKey = 'tbwave:' + hashDJB2(src);
679      var cached = getLocal(cacheKey);
680      var parsed = parseWaveJson(cached);
681      if (!parsed) continue;
682
683      var base = waveEl.querySelector('canvas.tb-wave__base');
684      var over = waveEl.querySelector('canvas.tb-wave__overlay');
685      if (!base || !over) continue;
686
687      drawWave(base, parsed.peaks, parsed.maxV, {
688        fill: getCssVar(waveEl, '--tb-wave-base-fill', 'rgba(15,23,42,.10)'),
689        line: getCssVar(waveEl, '--tb-wave-base-line', 'rgba(15,23,42,.25)'),
690        grid: getCssVar(waveEl, '--tb-wave-grid', 'rgba(15,23,42,.10)')
691      }, waveEl);
692
693      drawWave(over, parsed.peaks, parsed.maxV, {
694        fill: getCssVar(waveEl, '--tb-wave-fill', 'rgba(59,130,246,.35)'),
695        line: getCssVar(waveEl, '--tb-wave-line', 'rgba(59,130,246,.90)'),
696        grid: 'rgba(0,0,0,0)'
697      }, waveEl);
698    }
699  }
700
701  function watchDom() {
702    var root = document.querySelector('#dle-content') || document.body;
703    var mo = new MutationObserver(debounce(function () {
704      scan();
705    }, 120));
706    mo.observe(root, { childList: true, subtree: true });
707  }
708
709  function boot() {
710    injectStyles();
711    scan();
712    watchDom();
713
714    window.addEventListener('pageshow', function () {
715      setTimeout(scan, 0);
716      setTimeout(scan, 700);
717    });
718
719    window.addEventListener('resize', debounce(resizeAll, 140));
720
721    document.addEventListener('visibilitychange', function () {
722      if (!document.hidden) {
723        setTimeout(scan, 0);
724        setTimeout(scan, 600);
725      }
726    });
727  }
728
729  onReady(boot);
730})();

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.