/** * danmaku.js — NicoNico/弾幕-style flying comments for v0ck * * Usage: * const d = new Danmaku(playerEl, videoEl); * d.load(commentsArray); // feed comments from API * d.fire('hello!', 'user', '#f0f'); // fire one immediately * d.toggle(); // toggle on/off * d.destroy(); // cleanup */ (function () { const PILL_MIN_MS = 6000; // Fastest a pill can cross the screen const PILL_MAX_MS = 12000; // Slowest a pill can cross the screen const LOOKAHEAD_SEC = 0.25; // How far ahead of currentTime we look when scanning const MIN_RANDOM_SECS = 2; // Random timecode lower bound (avoid very start) const RANDOM_SPREAD = 0.85; // Use 85% of duration for random spread const DEFAULT_DANMAKU_TUNING = { fontSize: 35, opacity: 1.0, speedMultiplier: 1.0, laneCount: 10, laneCoverage: 100, fontWeight: 700, outlineStyle: 2, // 0=None, 1=Subtle, 2=Default Outline, 3=Neon Glow useCustomColor: 0, customColor: '#ffffff', pillBackground: 0, // 0=None, 1=Glass Card, 2=Solid Capsule allowMediaEmbeds: 1, mediaMaxHeight: 80, showGreentext: 1, densityLimit: 35, flashInterval: 2.5, showLaneGuides: 0, showDebugHUD: 0 }; const loadDanmakuTuning = () => { try { const raw = localStorage.getItem('f0ck_danmaku_tuning'); return raw ? Object.assign({}, DEFAULT_DANMAKU_TUNING, JSON.parse(raw)) : Object.assign({}, DEFAULT_DANMAKU_TUNING); } catch (e) { return Object.assign({}, DEFAULT_DANMAKU_TUNING); } }; window.DEFAULT_DANMAKU_TUNING = DEFAULT_DANMAKU_TUNING; window.danmakuTuning = window.danmakuTuning || loadDanmakuTuning(); /** * SyntheticClock — emulates a