// ==UserScript== // @name Bilibili 弹幕字号自适应与自定字重 // @namespace https://scriptcat.org/ // @version 0.1.0 // @description 根据 B 站播放器实际尺寸自动线性调整弹幕字号(兼顾官方字号设置与防挡脸),彻底修复全屏切小屏的轨道间距巨大 bug,并支持油猴菜单自定义字重与最大/最小字号。 // @author gymimc // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/bangumi/play/* // @match https://www.bilibili.com/medialist/play/* // @icon https://www.bilibili.com/favicon.ico // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; // ================= 1. 配置项初始化与本地缓存 ================= const DEFAULT_CONFIG = { baseWidth: 900, // 参照基准宽度(px) baseFontSize: 21, // 默认基准字号(px) maxFontSize: 36, // 最大绝对字号上限(px) minScale: 0.80, // 缩放下限(相当于最小字号 protection) customWeight: 400, // 默认字重:400 (标准细体) bigCustomWeight: 550, // 大字/全屏时的自定义字重:550 (适度中粗) enableBigBold: false, // 大字/全屏时是否启用自定义加粗 debounceDelay: 30 // 防抖延迟(毫秒) }; // 内存配置缓存对象,避免高频 resize 时频繁进行磁盘/扩展 IO 操作 let activeConfig = {}; function reloadConfig() { activeConfig = { baseWidth: GM_getValue('bili_dm_baseWidth', DEFAULT_CONFIG.baseWidth), baseFontSize: GM_getValue('bili_dm_baseFontSize', DEFAULT_CONFIG.baseFontSize), maxFontSize: GM_getValue('bili_dm_maxFontSize', DEFAULT_CONFIG.maxFontSize), minScale: GM_getValue('bili_dm_minScale', DEFAULT_CONFIG.minScale), customWeight: GM_getValue('bili_dm_customWeight', DEFAULT_CONFIG.customWeight), bigCustomWeight: GM_getValue('bili_dm_bigCustomWeight', DEFAULT_CONFIG.bigCustomWeight), enableBigBold: GM_getValue('bili_dm_enableBigBold', DEFAULT_CONFIG.enableBigBold), debounceDelay: DEFAULT_CONFIG.debounceDelay }; } // 初始化加载配置 reloadConfig(); // 状态全局变量 let dynamicStyleEl = null; let resizeObserver = null; let debounceTimer = null; // 1. 原生注入基础全局 CSS:强行压制探针中的 400px/1600px 盒模型高度,彻底修复间距与粗体 const baseStyleEl = document.createElement('style'); baseStyleEl.id = 'bili-dm-base-style'; baseStyleEl.innerHTML = ` div[class*="danmaku"], div[class*="dm-item"], div[class*="dm-rotate"], .bili-dm-player .dm-item, .bpx-player-dm-item { height: auto !important; /* 核心修复:摧毁 400px/1600px 的巨大容器,使间距紧凑 */ max-height: max-content !important; margin-top: 0px !important; margin-bottom: 0px !important; padding-top: 0px !important; padding-bottom: 0px !important; -webkit-text-stroke: 0px !important; /* 清除官方描边加粗 */ -webkit-font-smoothing: antialiased !important; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75) !important; } `; (document.head || document.documentElement).appendChild(baseStyleEl); // ================= 2. 核心逻辑函数 ================= /** * 高性能防抖函数 */ function debounce(fn, delay) { return function (...args) { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => fn.apply(this, args), delay); }; } /** * 根据播放器宽度进行平滑绝对像素计算并更新 CSS */ function updateDanmakuScale(containerWidth) { if (!containerWidth || containerWidth <= 0) return; // 1. 计算理论缩放比例 let rawScale = containerWidth / activeConfig.baseWidth; // 2. 计算物理字号,受 minScale 与 maxFontSize 共同约束 let rawFontSize = activeConfig.baseFontSize * Math.max(rawScale, activeConfig.minScale); let finalFontSize = Math.min(rawFontSize, activeConfig.maxFontSize).toFixed(1); // 3. 计算字重 (Font Weight) let targetWeight = activeConfig.customWeight; if (activeConfig.enableBigBold && finalFontSize >= (activeConfig.baseFontSize * 1.1)) { targetWeight = activeConfig.bigCustomWeight; } // 4. 动态写入样式节点 if (!dynamicStyleEl) { dynamicStyleEl = document.createElement('style'); dynamicStyleEl.id = 'bili-autoscale-danmaku-style'; (document.head || document.documentElement).appendChild(dynamicStyleEl); } dynamicStyleEl.innerHTML = ` div[class*="danmaku"], div[class*="dm-rotate"], .bili-dm-player .dm-item, .bpx-player-dm-item { font-size: ${finalFontSize}px !important; line-height: 1.15 !important; /* 紧凑行高,解决残留留白 */ font-weight: ${targetWeight} !important; } `; } /** * 获取播放器容器 DOM */ function getPlayerContainer() { return document.querySelector('.bpx-player-container') || document.querySelector('.bpx-player-video-area') || document.querySelector('.bilibili-player-video-wrap') || document.querySelector('#bilibili-player'); } /** * 重新刷新配置与应用样式 */ function refreshScale() { reloadConfig(); const playerContainer = getPlayerContainer(); if (playerContainer) { updateDanmakuScale(playerContainer.clientWidth); } } // ================= 3. 油猴/脚本猫 菜单注册 ================= function registerGMMenu() { // 1. 设置默认基准字号 GM_registerMenuCommand(`⚙️ 设置基准字号 (当前: ${activeConfig.baseFontSize}px)`, () => { const input = prompt(`请输入默认窗口(900px)下的基准字号(px) (默认: 21):`, activeConfig.baseFontSize); if (input !== null) { const val = parseFloat(input); if (!isNaN(val) && val >= 10 && val <= 40) { GM_setValue('bili_dm_baseFontSize', val); refreshScale(); alert(`基准字号已保存为: ${val}px`); } else { alert(`输入不合法!`); } } }); // 2. 设置全屏最大字号上限 GM_registerMenuCommand(`⚙️ 设置全屏最大字号 (当前: ${activeConfig.maxFontSize}px)`, () => { const input = prompt(`请输入全屏/大屏时的最大字号上限(px) (默认: 36,范围 20~60):`, activeConfig.maxFontSize); if (input !== null) { const val = parseFloat(input); if (!isNaN(val) && val >= 20 && val <= 60) { GM_setValue('bili_dm_maxFontSize', val); refreshScale(); alert(`最大字号上限已保存为: ${val}px`); } else { alert(`输入不合法!`); } } }); // 3. 设置常规字重数值 GM_registerMenuCommand(`✏️ 设置常规字重数值 (当前: ${activeConfig.customWeight})`, () => { const input = prompt(`请输入常规状态下的字重数值 (300=极细, 400=标准, 500=微粗, 默认: 400):`, activeConfig.customWeight); if (input !== null) { const val = parseInt(input, 10); if (!isNaN(val) && val >= 100 && val <= 900) { GM_setValue('bili_dm_customWeight', val); refreshScale(); alert(`常规字重已修改为: ${val}`); } else { alert(`输入不合法,请输入 100~900 之间的整数!`); } } }); // 4. 设置大字/全屏加粗数值 GM_registerMenuCommand(`✏️ 设置大字加粗数值 (当前: ${activeConfig.bigCustomWeight})`, () => { const input = prompt(`请输入大字/全屏状态下的加粗字重数值 (建议: 500~600,官方极粗为 900):`, activeConfig.bigCustomWeight); if (input !== null) { const val = parseInt(input, 10); if (!isNaN(val) && val >= 100 && val <= 900) { GM_setValue('bili_dm_bigCustomWeight', val); refreshScale(); alert(`大字加粗字重已修改为: ${val}`); } else { alert(`输入不合法,请输入 100~900 之间的整数!`); } } }); // 5. 切换大字加粗开关 const boldStatus = activeConfig.enableBigBold ? '【已开启】' : '【已关闭】'; GM_registerMenuCommand(`🎨 大字/全屏加粗开关 ${boldStatus}`, () => { const nextState = !activeConfig.enableBigBold; GM_setValue('bili_dm_enableBigBold', nextState); refreshScale(); alert(`已 ${nextState ? '开启' : '关闭'} 大字加粗!`); }); // 6. 恢复默认配置 GM_registerMenuCommand(`🔄 恢复默认配置`, () => { if (confirm(`确定要恢复默认配置吗?`)) { GM_setValue('bili_dm_baseFontSize', DEFAULT_CONFIG.baseFontSize); GM_setValue('bili_dm_maxFontSize', DEFAULT_CONFIG.maxFontSize); GM_setValue('bili_dm_customWeight', DEFAULT_CONFIG.customWeight); GM_setValue('bili_dm_bigCustomWeight', DEFAULT_CONFIG.bigCustomWeight); GM_setValue('bili_dm_enableBigBold', DEFAULT_CONFIG.enableBigBold); GM_setValue('bili_dm_minScale', DEFAULT_CONFIG.minScale); refreshScale(); alert(`已恢复默认设置!`); } }); } // ================= 4. 初始化与 DOM 监听 ================= function initObserver() { const playerContainer = getPlayerContainer(); if (!playerContainer) { // SPA 页面未载入完成时,循环轮询 setTimeout(initObserver, 500); return; } const handleResize = debounce((entries) => { for (let entry of entries) { updateDanmakuScale(entry.contentRect.width); } }, activeConfig.debounceDelay); // 彻底清理旧的 Observer 引用,防止 SPA 路由切换引起的内存泄漏 if (resizeObserver) { resizeObserver.disconnect(); resizeObserver = null; } resizeObserver = new ResizeObserver(handleResize); resizeObserver.observe(playerContainer); // 初始化更新一次 updateDanmakuScale(playerContainer.clientWidth); } // 启动逻辑 registerGMMenu(); initObserver(); // 适配 SPA 路由切换(点击推荐视频不刷新整页场景) let lastUrl = location.href; const urlObserver = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(initObserver, 1000); } }); urlObserver.observe(document.body, { childList: true, subtree: true }); })();