UNPKG

@scqilin/screen-scaler

Version:

A powerful screen scaling adapter for big screen applications with TypeScript support

493 lines (489 loc) 16.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); /** * 大屏缩放适配器 - TypeScript版本 * * 支持五种缩放模式: * - none: 默认模式,不做任何缩放处理 * - auto: 等比缩放,保持宽高比不变 * - width: 宽度适配,根据屏幕宽度等比缩放 * - height: 高度适配,根据屏幕高度等比缩放 * - stretch: 拉伸适配,分别缩放宽高以填满屏幕 * * 特点: * - 支持任意设计尺寸,通用性强 * - 自动处理窗口变化事件 * - 智能居中和滚动条控制 * - 提供详细的缩放信息回调 * - 内存安全,支持销毁和重新初始化 */ /** * 获取页面的滚动条宽度 * @returns */ function getScrollbarWidth() { const scrollDiv = document.createElement('div'); scrollDiv.style.cssText = 'width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;'; document.body.appendChild(scrollDiv); const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; document.body.removeChild(scrollDiv); return scrollbarWidth; } // 样式类名前缀 const CSS_PREFIX = 'screen-scaler'; let scrollbarWidth = getScrollbarWidth() + 1; class ScreenScaler { constructor(config) { this.rafId = null; this.isDestroyed = false; // 参数验证 this.validateConfig(config); // 初始化用户容器 const userContainer = typeof config.container === 'string' ? document.querySelector(config.container) : config.container; if (!userContainer) { throw new Error('Container element not found'); } this.container = userContainer; // 创建包裹容器和设置父级容器 this.setupContainerStructure(); // 设置默认配置 this.config = { container: this.container, designWidth: config.designWidth, designHeight: config.designHeight, mode: config.mode || 'auto', autoResize: config.autoResize !== false, onScaleChange: config.onScaleChange || (() => { }), }; // 绑定resize事件处理器 this.resizeHandler = this.handleResize.bind(this); // 初始化 this.init(); } /** * 设置容器结构 - 创建包裹容器 */ setupContainerStructure() { // 获取用户容器的父级元素 this.parentContainer = this.container.parentElement || document.body; // 创建包裹容器 this.wrapperContainer = document.createElement('div'); this.wrapperContainer.className = `${CSS_PREFIX}-wrapper`; // 设置包裹容器的基础样式 - 确保没有间隙 Object.assign(this.wrapperContainer.style, { position: 'relative', width: '100%', height: '100%', }); // 将用户容器插入到包裹容器中 // 1. 先将包裹容器插入到用户容器的位置 this.parentContainer.insertBefore(this.wrapperContainer, this.container); // 2. 然后将用户容器移动到包裹容器内 this.wrapperContainer.appendChild(this.container); } /** * 验证配置参数 */ validateConfig(config) { if (!config.container) { throw new Error('Container is required'); } if (!config.designWidth || config.designWidth <= 0) { throw new Error('designWidth must be a positive number'); } if (!config.designHeight || config.designHeight <= 0) { throw new Error('designHeight must be a positive number'); } } /** * 初始化缩放器 */ init() { // 设置容器初始样式 this.setupContainerStyles(); // 绑定事件 if (this.config.autoResize) { this.setupResizeObserver(); } this.handleResize(); } /** * 设置ResizeObserver监听包裹容器尺寸变化 */ setupResizeObserver() { window.addEventListener('resize', this.resizeHandler); } /** * 设置容器基础样式 */ setupContainerStyles() { const { designWidth, designHeight } = this.config; // 设置容器基础样式,确保没有默认间隙 Object.assign(this.container.style, { width: `${designWidth}px`, height: `${designHeight}px`, transformOrigin: '0 0', position: 'relative', margin: '0', padding: '0', border: 'none', boxSizing: 'border-box', }); } /** * 处理窗口大小变化 */ handleResize() { if (this.isDestroyed) return; // 使用 requestAnimationFrame 避免频繁计算 if (this.rafId) { cancelAnimationFrame(this.rafId); } this.rafId = requestAnimationFrame(() => { // 延迟一小段时间确保DOM完全更新 setTimeout(() => { this.setScale(); }, 100); }); } /** * 获取包裹容器的可用尺寸 */ getScreenSize() { // 强制刷新DOM尺寸计算 void this.wrapperContainer.offsetHeight; // 触发reflow // 临时设置overflow: hidden来获取不含滚动条的真实尺寸 const originalOverflow = this.wrapperContainer.style.overflow; this.wrapperContainer.style.overflow = 'hidden'; // 强制重新计算布局 void this.wrapperContainer.offsetHeight; // 获取真实的容器尺寸(不包含滚动条) let width = this.wrapperContainer.clientWidth; let height = this.wrapperContainer.clientHeight; // 恢复原始的overflow设置 this.wrapperContainer.style.overflow = originalOverflow; // 如果为0,则使用视口尺寸作为后备 if (width <= 0) { width = window.innerWidth; } if (height <= 0) { height = window.innerHeight; } return { width, height }; } /** * 重置所有样式到初始状态 */ resetStyles() { // 重置用户容器样式 Object.assign(this.container.style, { transform: 'none', position: 'relative', left: '0px', top: '0px', }); } /** * 默认模式 - 不做任何缩放处理 */ scaleNone() { const { designWidth, designHeight } = this.config; const screen = this.getScreenSize(); // 先重置所有样式 this.resetStyles(); return { mode: 'none', scale: { x: 1, y: 1, ratio: 1 }, screen, design: { width: designWidth, height: designHeight }, scaled: { width: designWidth, height: designHeight }, offset: { x: 0, y: 0 }, }; } /** * 等比缩放模式 */ scaleAuto() { const { designWidth, designHeight } = this.config; const screen = this.getScreenSize(); // 先重置所有样式 this.resetStyles(); this.wrapperContainer.style.overflow = 'hidden'; const scaleX = screen.width / designWidth; const scaleY = screen.height / designHeight; const scale = Math.min(scaleX, scaleY); // 计算缩放后尺寸和偏移 const scaledWidth = designWidth * scale; const scaledHeight = designHeight * scale; const offsetX = (screen.width - scaledWidth) / 2; const offsetY = (screen.height - scaledHeight) / 2; // 应用缩放和居中 Object.assign(this.container.style, { transform: `scale(${scale})`, position: 'absolute', left: `${offsetX}px`, top: `${offsetY}px`, }); return { mode: 'auto', scale: { x: scale, y: scale, ratio: scale }, screen, design: { width: designWidth, height: designHeight }, scaled: { width: scaledWidth, height: scaledHeight }, offset: { x: offsetX, y: offsetY }, }; } /** * 宽度适配模式 */ scaleWidth() { const { designWidth, designHeight } = this.config; // 先重置所有样式 this.resetStyles(); // 获取包裹容器的可用尺寸 const screen = this.getScreenSize(); let _w = screen.width; if (designWidth / designHeight < (screen.width) / screen.height) { _w -= scrollbarWidth; } const scale = _w / designWidth; const scaledWidth = designWidth * scale; const scaledHeight = designHeight * scale; // 应用缩放 this.container.style.transform = `scale(${scale})`; this.wrapperContainer.style.overflowX = 'clip'; const offsetX = 0; let offsetY = 0; // 智能居中和滚动处理 if (scaledHeight <= screen.height) { // 缩放后高度小于屏幕高度,垂直居中 offsetY = Math.floor((screen.height - scaledHeight) / 2); Object.assign(this.container.style, { position: 'absolute', left: '0px', top: `${offsetY}px`, }); } else { // 缩放后高度大于屏幕高度,允许垂直滚动 Object.assign(this.container.style, { position: 'relative', left: '0px', top: '0px', }); } return { mode: 'width', scale: { x: scale, y: scale, ratio: scale }, screen, design: { width: designWidth, height: designHeight }, scaled: { width: scaledWidth, height: scaledHeight }, offset: { x: offsetX, y: offsetY }, }; } /** * 高度适配模式 */ scaleHeight() { const { designWidth, designHeight } = this.config; // 先重置所有样式 this.resetStyles(); // 获取包裹容器的可用尺寸 const screen = this.getScreenSize(); let _h = screen.height; if (designWidth / designHeight > screen.width / screen.height) { _h -= scrollbarWidth; } // 高度适配:按照包裹容器高度进行等比缩放 const scale = _h / designHeight; const scaledWidth = designWidth * scale; const scaledHeight = designHeight * scale; // 应用缩放 this.container.style.transform = `scale(${scale})`; this.wrapperContainer.style.overflowY = 'clip'; let offsetX = 0; const offsetY = 0; // 智能居中和滚动处理 if (scaledWidth <= screen.width) { // 缩放后宽度小于屏幕宽度,水平居中 offsetX = Math.floor((screen.width - scaledWidth) / 2); Object.assign(this.container.style, { position: 'absolute', left: `${offsetX}px`, top: '0px', }); } else { // 缩放后宽度大于屏幕宽度,允许水平滚动 Object.assign(this.container.style, { position: 'relative', left: '0px', top: '0px', }); } return { mode: 'height', scale: { x: scale, y: scale, ratio: scale }, screen, design: { width: designWidth, height: designHeight }, scaled: { width: scaledWidth, height: scaledHeight }, offset: { x: offsetX, y: offsetY }, }; } /** * 拉伸适配模式 */ scaleStretch() { const { designWidth, designHeight } = this.config; const screen = this.getScreenSize(); // 先重置所有样式 this.resetStyles(); this.wrapperContainer.style.overflow = 'hidden'; const scaleX = screen.width / designWidth; const scaleY = screen.height / designHeight; // 应用不同比例的缩放 Object.assign(this.container.style, { transform: `scaleX(${scaleX}) scaleY(${scaleY})`, position: 'absolute', left: '0px', top: '0px', }); return { mode: 'stretch', scale: { x: scaleX, y: scaleY, ratio: Math.min(scaleX, scaleY) }, screen, design: { width: designWidth, height: designHeight }, scaled: { width: screen.width, height: screen.height }, offset: { x: 0, y: 0 }, }; } /** * 执行缩放 */ setScale() { if (this.isDestroyed) { throw new Error('ScreenScaler has been destroyed'); } let scaleInfo; scrollbarWidth = getScrollbarWidth() + 1; switch (this.config.mode) { case 'none': scaleInfo = this.scaleNone(); break; case 'auto': scaleInfo = this.scaleAuto(); break; case 'width': scaleInfo = this.scaleWidth(); break; case 'height': scaleInfo = this.scaleHeight(); break; case 'stretch': scaleInfo = this.scaleStretch(); break; default: throw new Error(`Unknown scale mode: ${this.config.mode}`); } // 触发回调 this.config.onScaleChange(scaleInfo); return scaleInfo; } /** * 设置缩放模式 */ setMode(mode) { if (this.isDestroyed) { throw new Error('ScreenScaler has been destroyed'); } this.config.mode = mode; return this.setScale(); } /** * 获取当前缩放模式 */ getMode() { return this.config.mode; } /** * 获取当前缩放信息 */ getScaleInfo() { return this.setScale(); } /** * 更新设计尺寸 */ updateDesignSize(width, height) { if (this.isDestroyed) { throw new Error('ScreenScaler has been destroyed'); } this.config.designWidth = width; this.config.designHeight = height; this.setupContainerStyles(); return this.setScale(); } /** * 启用/禁用自动resize */ setAutoResize(enabled) { if (this.isDestroyed) { throw new Error('ScreenScaler has been destroyed'); } if (enabled && !this.config.autoResize) { this.setupResizeObserver(); } else if (!enabled && this.config.autoResize) { window.removeEventListener('resize', this.resizeHandler); } this.config.autoResize = enabled; } /** * 设置缩放变化回调 */ setOnScaleChange(callback) { this.config.onScaleChange = callback; } /** * 销毁缩放器 */ destroy() { if (this.isDestroyed) return; // 取消resize事件监听 window.removeEventListener('resize', this.resizeHandler); // 取消待执行的动画帧 if (this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = null; } // 重置用户容器样式 Object.assign(this.container.style, { transform: '', position: '', left: '', top: '', width: '', height: '', }); // 恢复容器的原始位置结构 if (this.wrapperContainer && this.wrapperContainer.parentElement) { // 将用户容器移回到包裹容器的位置 this.wrapperContainer.parentElement.insertBefore(this.container, this.wrapperContainer); // 移除包裹容器 this.wrapperContainer.remove(); } // 标记为已销毁 this.isDestroyed = true; } /** * 检查是否已销毁 */ isDestroyed_() { return this.isDestroyed; } } exports.ScreenScaler = ScreenScaler; exports["default"] = ScreenScaler;