|
| 1 | +class TerminalBg { |
| 2 | + constructor (options = {}) { |
| 3 | + this.className = options.className || 'animate' |
| 4 | + this.color = options.color || '#666666' |
| 5 | + this.bgColor = options.bgColor || '#ffffff' |
| 6 | + this.fontSize = options.fontSize || 18 |
| 7 | + this.speed = options.speed || 0.8 |
| 8 | + this.lines = [] |
| 9 | + this.canvas = null |
| 10 | + this.ctx = null |
| 11 | + this.animationId = null |
| 12 | + this.lastTime = 0 |
| 13 | + this.frameInterval = 40 |
| 14 | + this.init() |
| 15 | + } |
| 16 | + |
| 17 | + getTexts () { |
| 18 | + return [ |
| 19 | + '终端/SSH/SFTP/Telnet/串口/RDP/VNC客户端', |
| 20 | + '全局热键切换窗口可见性', |
| 21 | + '多平台支持 Linux/Mac/Windows', |
| 22 | + '多语言支持', |
| 23 | + '双击直接编辑远程文件', |
| 24 | + '公钥+密码认证', |
| 25 | + '支持 Zmodem (rz/sz)', |
| 26 | + '支持 SSH 隧道', |
| 27 | + '支持 Trzsz (trz/tsz)', |
| 28 | + '透明窗口', |
| 29 | + '终端背景图片', |
| 30 | + '全局/会话代理', |
| 31 | + '快捷命令', |
| 32 | + 'UI/终端主题', |
| 33 | + '同步书签到 GitHub/Gitee', |
| 34 | + 'AI 助手集成', |
| 35 | + '支持 DeepSeek/OpenAI', |
| 36 | + '深度链接支持', |
| 37 | + '命令行使用', |
| 38 | + 'SSH/SFTP 文件传输', |
| 39 | + '批量操作', |
| 40 | + '多终端同步输入', |
| 41 | + 'VNC 远程桌面', |
| 42 | + 'RDP 远程桌面', |
| 43 | + '串口终端', |
| 44 | + 'Telnet 连接', |
| 45 | + 'FTP 客户端', |
| 46 | + 'Terminal/SSH/SFTP/Telnet/Serial/RDP/VNC client', |
| 47 | + 'Global hotkey to toggle window', |
| 48 | + 'Multi platform: Linux/Mac/Windows', |
| 49 | + 'Multi-language support', |
| 50 | + 'Double click to edit remote files', |
| 51 | + 'Auth with publicKey + password', |
| 52 | + 'Support Zmodem (rz/sz)', |
| 53 | + 'Support SSH tunnel', |
| 54 | + 'Support Trzsz (trz/tsz)', |
| 55 | + 'Transparent window', |
| 56 | + 'Terminal background image', |
| 57 | + 'Global/session proxy', |
| 58 | + 'Quick commands', |
| 59 | + 'UI/terminal themes', |
| 60 | + 'Sync to GitHub/Gitee gist', |
| 61 | + 'AI assistant integration', |
| 62 | + 'Support DeepSeek/OpenAI', |
| 63 | + 'Deep link support', |
| 64 | + 'Command line usage', |
| 65 | + 'SSH/SFTP file transfer', |
| 66 | + 'Batch operations', |
| 67 | + 'Multi-terminal sync input', |
| 68 | + 'VNC remote desktop', |
| 69 | + 'RDP remote desktop', |
| 70 | + 'Serial port terminal', |
| 71 | + 'Telnet connection', |
| 72 | + 'FTP client' |
| 73 | + ] |
| 74 | + } |
| 75 | + |
| 76 | + init () { |
| 77 | + let container = document.querySelector(`.${this.className}`) |
| 78 | + if (!container) { |
| 79 | + container = document.createElement('div') |
| 80 | + container.className = this.className |
| 81 | + document.body.insertBefore(container, document.body.firstChild) |
| 82 | + } |
| 83 | + container.style.cssText = 'position:fixed;left:0;right:0;top:0;bottom:0;z-index:-1;overflow:hidden;' |
| 84 | + this.container = container |
| 85 | + this.canvas = document.createElement('canvas') |
| 86 | + this.canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;' |
| 87 | + container.appendChild(this.canvas) |
| 88 | + this.ctx = this.canvas.getContext('2d') |
| 89 | + this.texts = this.getTexts() |
| 90 | + this.resize() |
| 91 | + window.addEventListener('resize', this.handleResize.bind(this)) |
| 92 | + this.animate() |
| 93 | + } |
| 94 | + |
| 95 | + handleResize () { |
| 96 | + if (this.resizeTimeout) { |
| 97 | + clearTimeout(this.resizeTimeout) |
| 98 | + } |
| 99 | + this.resizeTimeout = setTimeout(() => { |
| 100 | + this.resize() |
| 101 | + }, 100) |
| 102 | + } |
| 103 | + |
| 104 | + resize () { |
| 105 | + const container = this.canvas.parentElement |
| 106 | + const dpr = Math.min(window.devicePixelRatio || 1, 2) |
| 107 | + const rect = container.getBoundingClientRect() |
| 108 | + this.canvas.width = rect.width * dpr |
| 109 | + this.canvas.height = rect.height * dpr |
| 110 | + this.canvas.style.width = rect.width + 'px' |
| 111 | + this.canvas.style.height = rect.height + 'px' |
| 112 | + this.ctx.scale(dpr, dpr) |
| 113 | + this.width = rect.width |
| 114 | + this.height = rect.height |
| 115 | + this.initLines() |
| 116 | + } |
| 117 | + |
| 118 | + initLines () { |
| 119 | + const lineCount = Math.max(20, Math.floor(this.width / 80)) |
| 120 | + const texts = this.texts |
| 121 | + this.lines = [] |
| 122 | + const angle = Math.PI / 6 |
| 123 | + for (let i = 0; i < lineCount; i++) { |
| 124 | + const progress = i / lineCount |
| 125 | + const startX = -this.width * 0.5 + progress * (this.width * 1.5) |
| 126 | + const startY = -this.height * 0.3 + progress * (this.height * 1.3) - Math.random() * 150 |
| 127 | + this.lines.push({ |
| 128 | + x: startX, |
| 129 | + y: startY, |
| 130 | + angle, |
| 131 | + speed: (0.3 + Math.random() * 0.4) * this.speed, |
| 132 | + text: texts[Math.floor(Math.random() * texts.length)], |
| 133 | + opacity: 0.08 + Math.random() * 0.1, |
| 134 | + fontSize: this.fontSize + Math.floor(Math.random() * 6) |
| 135 | + }) |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + animate (timestamp = 0) { |
| 140 | + this.animationId = requestAnimationFrame(this.animate.bind(this)) |
| 141 | + const delta = timestamp - this.lastTime |
| 142 | + if (delta < this.frameInterval) return |
| 143 | + this.lastTime = timestamp - (delta % this.frameInterval) |
| 144 | + this.ctx.fillStyle = this.bgColor |
| 145 | + this.ctx.fillRect(0, 0, this.width, this.height) |
| 146 | + for (const line of this.lines) { |
| 147 | + const moveX = Math.cos(line.angle) * line.speed * 2 |
| 148 | + const moveY = Math.sin(line.angle) * line.speed * 2 |
| 149 | + line.x += moveX |
| 150 | + line.y += moveY |
| 151 | + if (line.y > this.height + 50 || line.x > this.width + 50) { |
| 152 | + line.x = -this.width * 0.5 + Math.random() * this.width * 0.3 |
| 153 | + line.y = -100 - Math.random() * 100 |
| 154 | + line.text = this.texts[Math.floor(Math.random() * this.texts.length)] |
| 155 | + line.opacity = 0.08 + Math.random() * 0.1 |
| 156 | + } |
| 157 | + this.ctx.save() |
| 158 | + this.ctx.translate(line.x, line.y) |
| 159 | + this.ctx.rotate(line.angle) |
| 160 | + this.ctx.font = `${line.fontSize}px monospace` |
| 161 | + this.ctx.fillStyle = this.hexToRgba(this.color, line.opacity) |
| 162 | + this.ctx.fillText(line.text, 0, 0) |
| 163 | + this.ctx.restore() |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + hexToRgba (hex, alpha) { |
| 168 | + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) |
| 169 | + if (result) { |
| 170 | + return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${alpha})` |
| 171 | + } |
| 172 | + const rgbMatch = hex.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) |
| 173 | + if (rgbMatch) { |
| 174 | + return `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, ${alpha})` |
| 175 | + } |
| 176 | + return `rgba(102, 102, 102, ${alpha})` |
| 177 | + } |
| 178 | + |
| 179 | + destroy () { |
| 180 | + if (this.animationId) { |
| 181 | + cancelAnimationFrame(this.animationId) |
| 182 | + } |
| 183 | + window.removeEventListener('resize', this.handleResize.bind(this)) |
| 184 | + if (this.canvas && this.canvas.parentElement) { |
| 185 | + this.canvas.parentElement.removeChild(this.canvas) |
| 186 | + } |
| 187 | + if (this.container && this.container.parentElement) { |
| 188 | + this.container.parentElement.removeChild(this.container) |
| 189 | + } |
| 190 | + } |
| 191 | +} |
| 192 | + |
| 193 | +export default TerminalBg |
0 commit comments