// ==UserScript== // @name B站视频旋转与缩放 // @namespace http://tampermonkey.net/&&https://scriptcat.org/zh-CN // @version 1.3.9 // @description 为B站视频添加旋转和缩放滑条控制,支持鼠标中键滚动调节,支持鼠标左键长按拖动调整位置(不触发视频暂停/播放) // @author John Smish // @match *://www.bilibili.com/* // @match *://bilibili.com/* // @match *://*.bilibili.com/* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // ==================== CSS样式 ==================== const style = document.createElement('style'); style.textContent = ` .bcmnp-rotate-box { box-sizing: border-box; position: absolute; width: 260px; height: fit-content; flex-direction: column; font-size: 12px; padding: 12px 16px 16px 16px; text-align: left; color: #fff; display: none; user-select: none; border-radius: 8px; background-color: rgba(0, 0, 0, 0.8); backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 9999; } .bpx-player-ctrl-rotate:hover { animation: none !important; } .bcmnp-rows { display: flex; flex-direction: column; gap: 20px; } .bcmnp-slider-container { display: flex; flex-direction: column; gap: 8px; } .bcmnp-label-row { display: flex; justify-content: space-between; align-items: center; } .bcmnp-label { font-size: 13px; font-weight: 500; color: rgba(255, 255, 255, 0.9); } .bcmnp-value { font-size: 13px; color: #00AEEC; font-weight: 500; } .bcmnp-slider { width: 100%; padding: 4px 0; } .bcmnp-slider input[type=range] { -webkit-appearance: none; width: 100%; height: 4px; background: rgba(255, 255, 255, 0.2); border-radius: 2px; outline: none; } .bcmnp-slider input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; background: white; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); border: 2px solid #00AEEC; transition: transform 0.1s ease; } .bcmnp-slider input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.15); } .bcmnp-scale-presets, .bcmnp-rotate-presets { display: flex; flex-direction: row; gap: 8px; margin-top: 4px; justify-content: space-between; } .bcmnp-preset-btn { background-color: rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 4px; width: 48px; height: 28px; line-height: 28px; text-align: center; transition: all 0.2s ease; color: rgba(255, 255, 255, 0.8); font-size: 12px; cursor: pointer; } .bcmnp-preset-btn:hover { background-color: rgba(255, 255, 255, 0.25); border-color: rgba(255, 255, 255, 0.2); } .bcmnp-preset-btn.active { background-color: #00AEEC; border-color: #00AEEC; color: white; box-shadow: 0 2px 8px rgba(0, 174, 236, 0.3); } /* 拖动提示样式 */ .bcmnp-drag-hint { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.8); color: white; padding: 8px 16px; border-radius: 20px; font-size: 14px; pointer-events: none; z-index: 10000; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.2); animation: bcmnp-fadeOut 1.5s ease forwards; white-space: nowrap; } @keyframes bcmnp-fadeOut { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } 10% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } } /* 拖动状态光标 - 应用到全局 */ body.bcmnp-dragging-active { cursor: grabbing !important; } .bcmnp-dragging-video { cursor: grabbing !important; } /* 重置位置按钮 */ .bcmnp-reset-position { margin-top: 8px; display: flex; justify-content: center; } .bcmnp-reset-btn { background-color: rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 4px; padding: 6px 12px; color: rgba(255, 255, 255, 0.9); font-size: 12px; cursor: pointer; transition: all 0.2s ease; width: 100%; text-align: center; } .bcmnp-reset-btn:hover { background-color: rgba(255, 255, 255, 0.25); border-color: rgba(255, 255, 255, 0.2); color: #00AEEC; } /* 按钮动画相关 */ @keyframes rotateToggle { 0% { transform: scale(1) translateX(0); } 25% { transform: scale(1) translateX(-3px); } 75% { transform: scale(1) translateX(-3px); } 100% { transform: scale(1) translateX(0); } } .bpx-player-ctrl-rotate.animating .bpx-player-ctrl-btn-icon { animation: rotateToggle 0.8s ease; } /* 拦截点击的覆盖层 */ .bcmnp-click-interceptor { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 99999; background: transparent; pointer-events: auto; display: none; } .bcmnp-click-interceptor.active { display: block; } /* 当前角度指示器 */ .bcmnp-angle-indicator { display: inline-block; width: 16px; height: 16px; border: 2px solid #00AEEC; border-radius: 50%; margin-left: 4px; position: relative; } .bcmnp-angle-indicator::after { content: ''; position: absolute; top: 50%; left: 50%; width: 2px; height: 6px; background-color: #00AEEC; transform: translate(-50%, -50%) rotate(var(--angle, 0deg)); transform-origin: center; } `; document.head.appendChild(style); // ==================== HTML模板 ==================== const rotateHtml = `
视频缩放 100%
视频旋转
`; // ==================== 工具函数 ==================== function waitUntilElementReady(selector) { return new Promise((resolve, reject) => { const maxTries = 100; let trys = 0; function _checkReady() { const el = document.querySelector(selector); if (el) { resolve(el); return; } if (trys++ > maxTries) { reject(new Error(`Element ${selector} not found`)); return; } setTimeout(_checkReady, 300); } _checkReady(); }); } function insertHtmlAfterElement(element, html) { const range = document.createRange(); const frag = range.createContextualFragment(html); element.parentElement?.insertBefore(frag, element.nextSibling); } function log(message) { console.log(`[B站视频旋转与缩放] ${message}`); } function printVersion(version, cost) { console.log( `%c 🎮 B站视频旋转与缩放 v${version} %c Cost ${cost}ms 作者:John Smish QQ:3327008209`, 'background:#4A90E2;color:white;padding:2px 6px;border-radius:3px 0 0 3px;font-weight:bold;', 'background:#50E3C2;color:#003333;padding:2px 6px;border-radius:0 3px 3px 0;font-weight:bold;', ); } function showDragHint(message = '按住左键拖动可移动视频位置') { const videoWrap = document.querySelector('.bpx-player-video-wrap'); if (!videoWrap) return; const hint = document.createElement('div'); hint.className = 'bcmnp-drag-hint'; hint.textContent = message; videoWrap.appendChild(hint); setTimeout(() => { if (hint.parentNode) { hint.remove(); } }, 1500); } // ==================== 旋转矩阵工具函数 ==================== function rotatePoint(x, y, angle) { const rad = angle * Math.PI / 180; const cos = Math.cos(rad); const sin = Math.sin(rad); return { x: x * cos - y * sin, y: x * sin + y * cos }; } // ==================== 动画关键帧 ==================== const rotateToggleKeyframes = [ { '@all': { scale: '1', translate: '0px 0px' }, '#bcmnp-toggle-icon-dot': { opacity: '1' }, '#bcmnp-toggle-icon-horizontal': { scale: '1', rotate: '0deg', transformOrigin: '50% 60%', translate: '0px 0px' }, '#bcmnp-toggle-icon-vertical': { opacity: '1' }, '#bcmnp-toggle-icon-shake': { opacity: '1', rotate: '0deg', transformOrigin: '50% 60%' } }, { '@all': { scale: '1', translate: '-3px 0px' }, '#bcmnp-toggle-icon-dot': { opacity: '0' }, '#bcmnp-toggle-icon-horizontal': { scale: '1.1', rotate: '90deg', transformOrigin: '50% 60%', translate: '200px -50px' }, '#bcmnp-toggle-icon-vertical': { opacity: '0' }, '#bcmnp-toggle-icon-shake': { opacity: '0', rotate: '0deg', transformOrigin: '50% 60%' } }, { '@all': { scale: '1', translate: '-3px 0px' }, '#bcmnp-toggle-icon-dot': { opacity: '0' }, '#bcmnp-toggle-icon-horizontal': { scale: '1.1', rotate: '90deg', transformOrigin: '50% 60%', translate: '200px -50px' }, '#bcmnp-toggle-icon-vertical': { opacity: '0' }, '#bcmnp-toggle-icon-shake': { opacity: '0', rotate: '180deg', transformOrigin: '50% 60%' } }, { '@all': { scale: '1', translate: '0px 0px' }, '#bcmnp-toggle-icon-dot': { opacity: '1' }, '#bcmnp-toggle-icon-horizontal': { scale: '1', rotate: '0deg', transformOrigin: '50% 60%', translate: '0px 0px' }, '#bcmnp-toggle-icon-vertical': { opacity: '1' }, '#bcmnp-toggle-icon-shake': { opacity: '1', rotate: '0deg', transformOrigin: '50% 60%' } } ]; function animateGroup(element, keyframes, options) { const individualKeyframes = []; for (const frame of keyframes) { for (const selector in frame) { let target = null; if (selector === '@all') { target = element; } else { target = element.querySelector(selector); } if (target) { let record = individualKeyframes.find(r => r.element === target); if (!record) { record = { element: target, keyframes: [] }; individualKeyframes.push(record); } record.keyframes.push(frame[selector]); } } } for (const { element: el, keyframes: kf } of individualKeyframes) { el.animate(kf, options); } } // ==================== 主控制器 ==================== class RotateController { constructor() { this.timer = null; this.isToggleAnimating = false; this.currentScale = 1; this.currentAngle = 0; this.currentTranslateX = 0; this.currentTranslateY = 0; this.updateTimer = null; this.lastEnterTime = 0; this.animationCooldown = false; // 拖动相关 this.isDragging = false; this.dragStartX = 0; this.dragStartY = 0; this.translateStartX = 0; this.translateStartY = 0; this.dragLongPressTimer = null; // 单击/长按检测器 this.isMouseDown = false; // 鼠标是否按下 this.mouseDownTime = 0; // 鼠标按下的时间戳 this.longPressThreshold = 500; // 长按阈值(毫秒) this.isLongPressReady = false; // 是否已满足长按条件(等待移动) // 用于拦截点击的标记 this.wasDragging = false; this.clickInterceptor = null; // 鼠标移动监听(全局) this.globalMouseMoveHandler = (e) => this.onGlobalMouseMove(e); this.globalMouseUpHandler = (e) => this.onGlobalMouseUp(e); this.init(); } init() { const toggle = document.querySelector('.bpx-player-ctrl-rotate .bpx-player-ctrl-btn-icon'); const panel = document.querySelector('.bpx-player-ctrl-rotate .bcmnp-rotate-box'); const scaleSlider = document.querySelector('#scale-slider'); const rotateSlider = document.querySelector('#rotate-slider'); const scaleValue = document.querySelector('#scale-value'); const rotateValue = document.querySelector('#rotate-value'); const scalePresets = document.querySelector('.bcmnp-scale-presets'); const rotatePresets = document.querySelector('.bcmnp-rotate-presets'); const rotateBtn = document.querySelector('.bpx-player-ctrl-rotate'); const resetBtn = document.querySelector('#reset-position-btn'); if (!toggle || !panel || !scaleSlider || !rotateSlider || !scaleValue || !rotateValue || !scalePresets || !rotatePresets || !rotateBtn || !resetBtn) { console.error('元素未找到'); return; } this.toggle = toggle; this.panel = panel; this.scaleSlider = scaleSlider; this.rotateSlider = rotateSlider; this.scaleValue = scaleValue; this.rotateValue = rotateValue; this.scalePresets = scalePresets; this.rotatePresets = rotatePresets; this.rotateBtn = rotateBtn; this.resetBtn = resetBtn; // 事件监听 this.rotateBtn.addEventListener('mouseenter', () => this.onMouseEnter()); this.rotateBtn.addEventListener('mouseleave', () => this.onMouseLeave()); this.panel.addEventListener('mouseenter', () => this.onPanelEnter()); this.panel.addEventListener('mouseleave', () => this.onPanelLeave()); this.scaleSlider.addEventListener('input', (e) => this.scaleSliderOnInput(e)); this.rotateSlider.addEventListener('input', (e) => this.rotateSliderOnInput(e)); this.scalePresets.addEventListener('click', (e) => this.scalePresetOnClick(e)); this.rotatePresets.addEventListener('click', (e) => this.rotatePresetOnClick(e)); this.resetBtn.addEventListener('click', () => this.resetPosition()); // 鼠标中键滚动控制滑条 this.scaleSlider.addEventListener('wheel', (e) => this.handleWheel(e, this.scaleSlider, 1)); this.rotateSlider.addEventListener('wheel', (e) => this.handleWheel(e, this.rotateSlider, 1)); const scaleContainer = this.scaleSlider.closest('.bcmnp-slider-container'); const rotateContainer = this.rotateSlider.closest('.bcmnp-slider-container'); if (scaleContainer) scaleContainer.addEventListener('wheel', (e) => this.handleWheel(e, this.scaleSlider, 1)); if (rotateContainer) rotateContainer.addEventListener('wheel', (e) => this.handleWheel(e, this.rotateSlider, 1)); // 创建点击拦截器 this.createClickInterceptor(); // 视频拖动相关事件 this.initDragEvents(); } createClickInterceptor() { this.clickInterceptor = document.createElement('div'); this.clickInterceptor.className = 'bcmnp-click-interceptor'; this.clickInterceptor.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); return false; }, true); this.clickInterceptor.addEventListener('mousedown', (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); return false; }, true); this.clickInterceptor.addEventListener('mouseup', (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); return false; }, true); document.body.appendChild(this.clickInterceptor); } initDragEvents() { const videoWrap = document.querySelector('.bpx-player-video-wrap'); if (!videoWrap) { setTimeout(() => this.initDragEvents(), 1000); return; } const video = videoWrap.querySelector('video'); if (video) { video.addEventListener('click', (e) => this.onVideoClick(e), true); video.addEventListener('mousedown', (e) => this.onVideoMouseDown(e), true); video.addEventListener('mouseup', (e) => this.onVideoMouseUp(e), true); video.addEventListener('mousemove', (e) => this.onVideoMouseMove(e), true); } videoWrap.addEventListener('mousedown', (e) => this.onVideoMouseDown(e), true); videoWrap.addEventListener('mouseup', (e) => this.onVideoMouseUp(e), true); videoWrap.addEventListener('mousemove', (e) => this.onVideoMouseMove(e), true); videoWrap.addEventListener('dragstart', (e) => e.preventDefault()); // 添加全局 mouseup 监听,确保在任何地方松开鼠标都能退出拖动状态 document.addEventListener('mouseup', this.globalMouseUpHandler, true); setTimeout(() => showDragHint(), 3000); } onVideoClick(e) { // 计算按下到松开的时间 const clickDuration = Date.now() - this.mouseDownTime; // 如果是短按(小于阈值)且没有拖动,让播放/暂停正常触发 if (clickDuration < this.longPressThreshold && !this.isDragging && !this.wasDragging) { log(`单击检测: ${clickDuration}ms,允许播放/暂停`); return; // 不阻止,让事件正常传递 } // 如果是长按或拖动,阻止点击事件 e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); log(`阻止点击事件 - 时长: ${clickDuration}ms, 长按就绪: ${this.isLongPressReady}, 拖动: ${this.isDragging}`); return false; } onVideoMouseDown(e) { if (e.button !== 0) return; log('鼠标按下'); // 重置所有状态 this.isLongPressReady = false; this.isDragging = false; this.wasDragging = false; // 记录鼠标按下状态和时间 this.isMouseDown = true; this.mouseDownTime = Date.now(); // 清除之前的定时器 if (this.dragLongPressTimer) { clearTimeout(this.dragLongPressTimer); this.dragLongPressTimer = null; } // 保存起始位置 this.dragStartX = e.clientX; this.dragStartY = e.clientY; this.translateStartX = this.currentTranslateX; this.translateStartY = this.currentTranslateY; // 设置长按检测定时器 this.dragLongPressTimer = setTimeout(() => { // 如果鼠标仍然按着,标记为长按就绪 if (this.isMouseDown) { this.isLongPressReady = true; log('长按就绪: 500ms已到,等待移动激活拖动'); // 添加全局鼠标移动监听 document.addEventListener('mousemove', this.globalMouseMoveHandler, true); } }, this.longPressThreshold); // 不阻止默认行为 } onVideoMouseMove(e) { // 这个方法现在主要用于调试,实际移动处理在全局监听中 if (this.isDragging) { e.preventDefault(); e.stopPropagation(); } } onVideoMouseUp(e) { if (e.button !== 0) return; log('鼠标松开 (video)'); this.endDrag(e); } onGlobalMouseUp(e) { if (e.button !== 0) return; log('鼠标松开 (global)'); this.endDrag(e); } endDrag(e) { // 鼠标已松开 this.isMouseDown = false; // 清除长按定时器 if (this.dragLongPressTimer) { clearTimeout(this.dragLongPressTimer); this.dragLongPressTimer = null; } // 如果是拖动状态,处理拖动结束 if (this.isDragging) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); this.wasDragging = true; this.isDragging = false; this.isLongPressReady = false; const videoWrap = document.querySelector('.bpx-player-video-wrap'); if (videoWrap) { videoWrap.classList.remove('bcmnp-dragging-video'); } document.body.classList.remove('bcmnp-dragging-active'); // 移除全局鼠标监听 document.removeEventListener('mousemove', this.globalMouseMoveHandler, true); if (this.clickInterceptor) { setTimeout(() => { this.clickInterceptor.classList.remove('active'); }, 200); } log(`拖动结束,位置: (${this.currentTranslateX.toFixed(1)}, ${this.currentTranslateY.toFixed(1)})`); setTimeout(() => { this.wasDragging = false; }, 300); } else { // 如果不是拖动,清理状态 this.isLongPressReady = false; document.removeEventListener('mousemove', this.globalMouseMoveHandler, true); } } onGlobalMouseMove(e) { // 如果还没有长按就绪,或者鼠标已松开,不处理 if (!this.isLongPressReady || !this.isMouseDown) { return; } e.preventDefault(); e.stopPropagation(); // 计算从按下位置开始的移动距离 const deltaX = Math.abs(e.clientX - this.dragStartX); const deltaY = Math.abs(e.clientY - this.dragStartY); // 如果移动超过阈值,激活拖动 if (deltaX > 3 || deltaY > 3) { // 如果还没有激活拖动,现在激活 if (!this.isDragging) { this.isDragging = true; const videoWrap = document.querySelector('.bpx-player-video-wrap'); if (videoWrap) { // 重新获取当前的translate值,确保起始位置正确 const transform = videoWrap.style.transform; const translateMatch = transform.match(/translate\(([-\d.]+)px,\s*([-\d.]+)px\)/); if (translateMatch) { this.translateStartX = parseFloat(translateMatch[1]) || 0; this.translateStartY = parseFloat(translateMatch[2]) || 0; } document.body.classList.add('bcmnp-dragging-active'); videoWrap.classList.add('bcmnp-dragging-video'); if (this.clickInterceptor) { this.clickInterceptor.classList.add('active'); } log(`拖动激活,起始位置: (${this.translateStartX}, ${this.translateStartY})`); } } // 处理拖动移动 if (this.isDragging) { // 计算鼠标在屏幕上的移动距离 const moveDeltaX = e.clientX - this.dragStartX; const moveDeltaY = e.clientY - this.dragStartY; // 根据当前旋转角度,将屏幕坐标的移动转换为视频坐标系中的移动 const rotatedDelta = rotatePoint(moveDeltaX, moveDeltaY, -this.currentAngle); // 根据缩放比例调整移动速度 const moveFactor = 1 / this.currentScale; const adjustedDeltaX = rotatedDelta.x * moveFactor; const adjustedDeltaY = rotatedDelta.y * moveFactor; // 计算新的位置 const newTranslateX = this.translateStartX + adjustedDeltaX; const newTranslateY = this.translateStartY + adjustedDeltaY; // 更新位置 this.currentTranslateX = newTranslateX; this.currentTranslateY = newTranslateY; // 应用到视频 this.updateVideoTransform(); } } } resetPosition() { this.currentTranslateX = 0; this.currentTranslateY = 0; this.updateVideoTransform(); showDragHint('位置已重置'); log('位置重置'); } handleWheel(e, slider, step = 1) { e.preventDefault(); e.stopPropagation(); const delta = e.deltaY > 0 ? -step : step; let newValue = parseInt(slider.value, 10) + delta; const min = parseInt(slider.min, 10); const max = parseInt(slider.max, 10); newValue = Math.max(min, Math.min(max, newValue)); slider.value = newValue.toString(); const inputEvent = new Event('input', { bubbles: true }); slider.dispatchEvent(inputEvent); } onMouseEnter() { const now = Date.now(); if (this.timer) { clearTimeout(this.timer); this.timer = null; } this.showPanel(); if (!this.animationCooldown || (now - this.lastEnterTime > 1000)) { this.playAnimation(); this.lastEnterTime = now; this.animationCooldown = true; setTimeout(() => { this.animationCooldown = false; }, 1500); } } onMouseLeave() { this.startHideTimer(); } onPanelEnter() { if (this.timer) { clearTimeout(this.timer); this.timer = null; } } onPanelLeave() { this.startHideTimer(); } startHideTimer() { if (this.timer) { clearTimeout(this.timer); } this.timer = setTimeout(() => { this.hidePanel(); this.timer = null; }, 300); } playAnimation() { if (!this.isToggleAnimating) { this.isToggleAnimating = true; animateGroup(this.toggle, rotateToggleKeyframes, { duration: 800, easing: 'ease' }); setTimeout(() => { this.isToggleAnimating = false; }, 1000); } } showPanel() { if (this.panel.style.display === 'flex') return; this.panel.style.display = 'flex'; this.updatePanelPosition(); } hidePanel() { if (this.panel.style.display === 'none') return; this.panel.style.display = 'none'; } updatePanelPosition() { const toggleRect = this.rotateBtn.getBoundingClientRect(); const panelRect = this.panel.getBoundingClientRect(); const screenType = document.querySelector('.bpx-player-container')?.dataset.screen ?? 'normal'; this.panel.style.bottom = (screenType === 'full' || screenType === 'web') ? '74px' : '41px'; this.panel.style.right = `${(toggleRect.width - panelRect.width) / 2}px`; } scaleSliderOnInput(e) { const value = parseInt(e.target.value, 10); this.scaleValue.textContent = `${value}%`; this.currentScale = value / 100; this.updateActivePresets(); if (this.updateTimer) clearTimeout(this.updateTimer); this.updateTimer = setTimeout(() => this.updateVideoTransform(), 50); } rotateSliderOnInput(e) { const value = parseInt(e.target.value, 10); this.rotateValue.textContent = `${value}°`; this.currentAngle = value; this.updateActivePresets(); if (this.updateTimer) clearTimeout(this.updateTimer); this.updateTimer = setTimeout(() => this.updateVideoTransform(), 50); } scalePresetOnClick(e) { const target = e.target; if (!target.classList.contains('bcmnp-preset-btn')) return; const scaleStr = target.dataset.scale; if (!scaleStr) return; const scale = parseFloat(scaleStr); const percent = Math.round(scale * 100); this.scaleSlider.value = percent.toString(); this.scaleValue.textContent = `${percent}%`; this.currentScale = scale; this.updateActivePresets(); this.updateVideoTransform(); } rotatePresetOnClick(e) { const target = e.target; if (!target.classList.contains('bcmnp-preset-btn')) return; const angleStr = target.dataset.angle; if (!angleStr) return; const angle = parseInt(angleStr, 10); this.rotateSlider.value = angle.toString(); this.rotateValue.textContent = `${angle}°`; this.currentAngle = angle; this.updateActivePresets(); this.updateVideoTransform(); } updateActivePresets() { this.scalePresets.querySelectorAll('.bcmnp-preset-btn').forEach(btn => { const scaleStr = btn.dataset.scale; if (scaleStr) { const scale = parseFloat(scaleStr); btn.classList.toggle('active', Math.abs(scale - this.currentScale) < 0.01); } }); this.rotatePresets.querySelectorAll('.bcmnp-preset-btn').forEach(btn => { const angleStr = btn.dataset.angle; if (angleStr) { const angle = parseInt(angleStr, 10); btn.classList.toggle('active', angle === this.currentAngle); } }); } updateVideoTransform() { const video = document.querySelector('.bpx-player-video-wrap'); if (!video) { log('视频元素未找到'); return; } let angle = this.currentAngle; const scale = this.currentScale; const W = 16, H = 9; const rad = angle * Math.PI / 180; const scaleX = W / (W * Math.abs(Math.cos(rad)) + H * Math.abs(Math.sin(rad))); const scaleY = H / (W * Math.abs(Math.sin(rad)) + H * Math.abs(Math.cos(rad))); const compositeScale = Math.min(scaleX, scaleY) * scale; if (angle === 90 || angle === 270 || angle === 180) angle += 0.0001; const oldTransform = video.style.transform || ''; const newTransform = `rotate(${angle}deg) scale(${compositeScale}) translate(${this.currentTranslateX}px, ${this.currentTranslateY}px)`; video.style.transform = newTransform; video.style.transformOrigin = 'center center'; if (!this.isDragging && oldTransform) { video.animate([ { transform: oldTransform }, { transform: newTransform } ], { duration: 300, easing: 'ease-in-out' }); } } } // ==================== 启动脚本 ==================== async function main() { try { const settingBtn = await waitUntilElementReady('.bpx-player-ctrl-btn.bpx-player-ctrl-setting'); const beginTime = performance.now(); insertHtmlAfterElement(settingBtn, rotateHtml); setTimeout(() => { window.rotateController = new RotateController(); const cost = (performance.now() - beginTime).toFixed(1); printVersion('1.3.9', cost); log('脚本加载完成!功能:旋转、缩放、鼠标中键调节、左键长按500ms后移动才激活拖动(已修复松开退出拖动状态)'); }, 500); } catch (error) { console.error('脚本初始化失败:', error); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); } })();