| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- // ============================================================================
- // led.js — LED 点阵滚动文字
- //
- // 读取后端 GetLEDText() 获取要显示的文字,转换为 LED 点阵(数字/字母/符号
- // 用内置 5x7 字模,汉字用 Canvas 像素采样),在画布上横向循环滚动。
- // ============================================================================
- import {SettingsService} from "../bindings/TengTuToolbox/index.js";
- // ---- 5x7 点阵字模(每字符 7 行,每行 5 位,高位在左)----
- const FONT_5x7 = {
- '0': ['01110','10001','10011','10101','11001','10001','01110'],
- '1': ['00100','01100','00100','00100','00100','00100','01110'],
- '2': ['01110','10001','00001','00010','00100','01000','11111'],
- '3': ['11110','00001','00001','01110','00001','00001','11110'],
- '4': ['00010','00110','01010','10010','11111','00010','00010'],
- '5': ['11111','10000','11110','00001','00001','10001','01110'],
- '6': ['00110','01000','10000','11110','10001','10001','01110'],
- '7': ['11111','00001','00010','00100','01000','01000','01000'],
- '8': ['01110','10001','10001','01110','10001','10001','01110'],
- '9': ['01110','10001','10001','01111','00001','00010','01100'],
- 'A': ['01110','10001','10001','11111','10001','10001','10001'],
- 'B': ['11110','10001','10001','11110','10001','10001','11110'],
- 'C': ['01110','10001','10000','10000','10000','10001','01110'],
- 'D': ['11100','10010','10001','10001','10001','10010','11100'],
- 'E': ['11111','10000','10000','11110','10000','10000','11111'],
- 'F': ['11111','10000','10000','11110','10000','10000','10000'],
- 'G': ['01110','10001','10000','10111','10001','10001','01111'],
- 'H': ['10001','10001','10001','11111','10001','10001','10001'],
- 'I': ['01110','00100','00100','00100','00100','00100','01110'],
- 'J': ['00111','00010','00010','00010','00010','10010','01100'],
- 'K': ['10001','10010','10100','11000','10100','10010','10001'],
- 'L': ['10000','10000','10000','10000','10000','10000','11111'],
- 'M': ['10001','11011','10101','10101','10001','10001','10001'],
- 'N': ['10001','11001','10101','10011','10001','10001','10001'],
- 'O': ['01110','10001','10001','10001','10001','10001','01110'],
- 'P': ['11110','10001','10001','11110','10000','10000','10000'],
- 'Q': ['01110','10001','10001','10001','10101','10010','01101'],
- 'R': ['11110','10001','10001','11110','10100','10010','10001'],
- 'S': ['01111','10000','10000','01110','00001','00001','11110'],
- 'T': ['11111','00100','00100','00100','00100','00100','00100'],
- 'U': ['10001','10001','10001','10001','10001','10001','01110'],
- 'V': ['10001','10001','10001','10001','10001','01010','00100'],
- 'W': ['10001','10001','10001','10101','10101','10101','01010'],
- 'X': ['10001','10001','01010','00100','01010','10001','10001'],
- 'Y': ['10001','10001','01010','00100','00100','00100','00100'],
- 'Z': ['11111','00001','00010','00100','01000','10000','11111'],
- 'a': ['00000','00000','01110','00001','01111','10001','01111'],
- 'b': ['10000','10000','11110','10001','10001','10001','11110'],
- 'c': ['00000','00000','01110','10001','10000','10001','01110'],
- 'd': ['00001','00001','01111','10001','10001','10001','01111'],
- 'e': ['00000','00000','01110','10001','11111','10000','01110'],
- 'f': ['00110','01001','01000','11100','01000','01000','01000'],
- 'g': ['00000','01111','10001','10001','01111','00001','01110'],
- 'h': ['10000','10000','11110','10001','10001','10001','10001'],
- 'i': ['00100','00000','01100','00100','00100','00100','01110'],
- 'j': ['00010','00000','00110','00010','00010','10010','01100'],
- 'k': ['10000','10000','10010','10100','11000','10100','10010'],
- 'l': ['01100','00100','00100','00100','00100','00100','01110'],
- 'm': ['00000','00000','11010','10101','10101','10101','10101'],
- 'n': ['00000','00000','11110','10001','10001','10001','10001'],
- 'o': ['00000','00000','01110','10001','10001','10001','01110'],
- 'p': ['00000','00000','11110','10001','11110','10000','10000'],
- 'q': ['00000','00000','01111','10001','01111','00001','00001'],
- 'r': ['00000','00000','10110','11001','10000','10000','10000'],
- 's': ['00000','00000','01111','10000','01110','00001','11110'],
- 't': ['01000','01000','11100','01000','01000','01001','00110'],
- 'u': ['00000','00000','10001','10001','10001','10011','01101'],
- 'v': ['00000','00000','10001','10001','10001','01010','00100'],
- 'w': ['00000','00000','10001','10001','10101','10101','01010'],
- 'x': ['00000','00000','10001','01010','00100','01010','10001'],
- 'y': ['00000','00000','10001','10001','01111','00001','01110'],
- 'z': ['00000','00000','11111','00010','00100','01000','11111'],
- ' ': ['00000','00000','00000','00000','00000','00000','00000'],
- '.': ['00000','00000','00000','00000','00000','00110','00110'],
- ',': ['00000','00000','00000','00000','00000','00110','00100'],
- ':': ['00000','00110','00110','00000','00110','00110','00000'],
- ';': ['00000','00110','00110','00000','00110','00100','01000'],
- '-': ['00000','00000','00000','11111','00000','00000','00000'],
- '_': ['00000','00000','00000','00000','00000','00000','11111'],
- '+': ['00000','00100','00100','11111','00100','00100','00000'],
- '=': ['00000','00000','11111','00000','11111','00000','00000'],
- '/': ['00001','00010','00010','00100','01000','01000','10000'],
- '\\':['10000','01000','01000','00100','00010','00010','00001'],
- '!': ['00100','00100','00100','00100','00100','00000','00100'],
- '?': ['01110','10001','00001','00010','00100','00000','00100'],
- '@': ['01110','10001','10111','10101','10111','10000','01110'],
- '#': ['01010','01010','11111','01010','11111','01010','01010'],
- '$': ['00100','01111','10100','01110','00101','11110','00100'],
- '%': ['11001','11010','00010','00100','01000','01011','10011'],
- '&': ['01100','10010','10100','01000','10101','10010','01101'],
- '*': ['00000','00100','10101','01110','10101','00100','00000'],
- '(': ['00010','00100','01000','01000','01000','00100','00010'],
- ')': ['01000','00100','00010','00010','00010','00100','01000'],
- '[': ['01110','01000','01000','01000','01000','01000','01110'],
- ']': ['01110','00010','00010','00010','00010','00010','01110'],
- '{': ['00010','00100','00100','01000','00100','00100','00010'],
- '}': ['01000','00100','00100','00010','00100','00100','01000'],
- '<': ['00010','00100','01000','10000','01000','00100','00010'],
- '>': ['01000','00100','00010','00001','00010','00100','01000'],
- '|': ['00100','00100','00100','00100','00100','00100','00100'],
- '~': ['00000','01000','10101','00010','00000','00000','00000'],
- '^': ['00100','01010','10001','00000','00000','00000','00000'],
- '“': ['01010','01010','00100','00100','00100','00100','00100'],
- '”': ['01010','01010','00100','00100','00100','00100','00100'],
- '‘': ['00100','01010','01010','00100','00100','00100','00100'],
- '’': ['00100','01010','01010','00100','00100','00100','00100'],
- '。': ['00000','00000','00000','00000','00110','00110','00000'],
- ',': ['00000','00000','00000','00000','00110','00100','01000'],
- '、': ['00100','00100','00100','00100','00100','00000','00000'],
- ':': ['00000','00110','00110','00000','00110','00110','00000'],
- ';': ['00000','00110','00110','00000','00110','00100','01000'],
- '?': ['01110','10001','00001','00010','00100','00000','00100'],
- '!': ['00100','00100','00100','00100','00100','00000','00100'],
- '(': ['00010','00100','01000','01000','01000','00100','00010'],
- ')': ['01000','00100','00010','00010','00010','00100','01000'],
- '《': ['00010','00100','01000','10000','01000','00100','00010'],
- '》': ['01000','00100','00010','00001','00010','00100','01000'],
- '“': ['00100','01010','01010','00100','00100','00100','00100'],
- };
- // 汉字点阵:用 Canvas 把文字画出来,按 grid 采样。缓存避免重复计算。
- const hanziCache = new Map();
- // 将 5x7 字模放大 scale 倍(每点变 scale×scale 块),用于提高点阵密度
- function scaleGlyph(rows, scale) {
- const h = rows.length * scale, w = rows[0].length * scale;
- const out = [];
- for (let y = 0; y < h; y++) {
- const line = [];
- for (let x = 0; x < w; x++) {
- line.push(rows[Math.floor(y / scale)][Math.floor(x / scale)]);
- }
- out.push(line);
- }
- return out;
- }
- // 将单个字符渲染为点阵数组 [{row, col, on}],返回宽高(宽=列数,高=行数)
- function charToBitmap(ch) {
- const key = ch;
- if (hanziCache.has(key)) return hanziCache.get(key);
- let rows = null;
- const upper = ch.toUpperCase();
- // 英文/数字/符号:优先用字模,放大 2 倍提高密度(5x7 → 10x14)
- if (FONT_5x7[ch]) {
- rows = scaleGlyph(FONT_5x7[ch].map(line => line.split('').map(b => b === '1')), 2);
- return {w: 10, h: 14, on: rows, char: ch};
- }
- if (FONT_5x7[upper]) {
- rows = scaleGlyph(FONT_5x7[upper].map(line => line.split('').map(b => b === '1')), 2);
- return {w: 10, h: 14, on: rows, char: ch};
- }
- // 汉字/其他:Canvas 像素采样为 24x32 点阵(密度是原 12x16 的 2 倍)
- const gw = 24, gh = 32;
- const off = document.createElement('canvas');
- off.width = gw; off.height = gh;
- const octx = off.getContext('2d');
- octx.fillStyle = '#fff';
- octx.font = `${gh * 0.9}px "Microsoft YaHei", "SimHei", sans-serif`;
- octx.textAlign = 'center';
- octx.textBaseline = 'middle';
- octx.fillText(ch, gw / 2, gh / 2 + 1);
- const data = octx.getImageData(0, 0, gw, gh).data;
- const on = [];
- for (let y = 0; y < gh; y++) {
- const line = [];
- for (let x = 0; x < gw; x++) {
- line.push(data[(y * gw + x) * 4 + 3] > 100);
- }
- on.push(line);
- }
- const r = {w: gw, h: gh, on, char: ch};
- hanziCache.set(key, r);
- return r;
- }
- // 把整串文字拆成字符点阵序列(含间距),返回 [{char, on, w, h}]
- function textToSequence(text) {
- const seq = [];
- const chars = [...text];
- for (const ch of chars) {
- const bmp = charToBitmap(ch);
- seq.push({char: ch, on: bmp.on, w: bmp.w, h: bmp.h});
- }
- return seq;
- }
- // 计算整行点阵的总宽(列数)与最大高(行数)
- function layout(seq, gap) {
- let total = 0, maxH = 0;
- for (let i = 0; i < seq.length; i++) {
- total += seq[i].w;
- if (i < seq.length - 1) total += gap;
- if (seq[i].h > maxH) maxH = seq[i].h;
- }
- return {total, maxH};
- }
- // 主渲染:canvas 上绘制点阵,requestAnimationFrame 横向无缝滚动循环
- const canvas = document.getElementById('led-canvas');
- const ctx = canvas.getContext('2d');
- let LED_COLOR = '#ff4d4d'; // LED 点阵颜色(可从后端读取,可设置)
- const GAP = 3; // 字符间距(点)
- const CHAR_GAP = 2; // 字符内点距
- const DOT_RADIUS = 0.47; // 点半径占点距比例(接近 0.5 更饱满,圆形 LED)
- let seq = [], maxRows = 0;
- let speed = 60; // 像素/秒(CSS 像素)
- let offsetX = 0; // 当前水平偏移(CSS 像素,负=向左滚)
- let lastTs = 0;
- let dpr = 1; // 设备像素比
- // 把 #rrggbb 颜色拆成 {r,g,b},方便做发光渐变
- function hexToRgb(hex) {
- const m = /^#?([0-9a-f]{6})$/i.exec(hex || '');
- if (!m) return {r: 255, g: 77, b: 77};
- const n = parseInt(m[1], 16);
- return {r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255};
- }
- // 按当前窗口逻辑高度计算点距(点阵单元尺寸)
- function dotSize(H) {
- // 上下安全边距(CSS 像素),保证首末行笔划完整显示、不被窗口裁剪
- const pad = 6;
- const avail = Math.max(1, H - 2 * pad);
- // 点阵总高 = maxRows * (dot + CHAR_GAP),须 <= avail,否则垂直居中后
- // baseY 为负、顶部笔划被裁。向下取整保证数学上不溢出。
- let dot = Math.floor((avail - CHAR_GAP * maxRows) / maxRows);
- // 极高 DPI 缩放或窗口过矮导致可用高度不足以容纳满格点阵时,
- // 不强制抬升 dot(旧的 Math.max(3,...) 会把 dot 抬回 3,使总高溢出、
- // baseY 变负、顶部笔划被窗口裁剪),而是允许缩小到最小 1,
- // 确保 baseY 不为负、点阵完整显示。
- if (dot < 1) dot = 1;
- return dot;
- }
- // 整行文字的像素宽度(含字符间与末尾间隙),用于计算无缝循环周期
- function totalPxWidth(dot) {
- let w = 0;
- for (const item of seq) {
- w += item.w * (dot + CHAR_GAP) + GAP * dot; // 每字符点阵宽 + 字后间隙
- }
- return w;
- }
- // 绘制单个 LED 点:中心高亮 + 外圈光晕,模拟真实 LED 灯珠
- function drawLedDot(cx, cy, r, color) {
- const rgb = hexToRgb(color);
- // 高亮中心(更亮)
- ctx.fillStyle = `rgba(${Math.min(255, rgb.r + 60)},${Math.min(255, rgb.g + 60)},${Math.min(255, rgb.b + 60)},1)`;
- ctx.beginPath();
- ctx.arc(cx, cy, r * 0.72, 0, Math.PI * 2);
- ctx.fill();
- // 外圈光晕(径向渐变:中心亮、边缘淡,聚焦发光感)
- const grad = ctx.createRadialGradient(cx, cy, r * 0.3, cx, cy, r);
- grad.addColorStop(0, `rgba(${rgb.r},${rgb.g},${rgb.b},0.85)`);
- grad.addColorStop(0.7, `rgba(${rgb.r},${rgb.g},${rgb.b},0.35)`);
- grad.addColorStop(1, `rgba(${rgb.r},${rgb.g},${rgb.b},0.05)`);
- ctx.fillStyle = grad;
- ctx.beginPath();
- ctx.arc(cx, cy, r, 0, Math.PI * 2);
- ctx.fill();
- }
- // 绘制一遍文字,起始横坐标为 x0(CSS 像素)
- function drawText(x0, H, dot) {
- let x = x0;
- const cell = dot + CHAR_GAP; // 点阵单元尺寸
- const r = dot * DOT_RADIUS; // 圆形点半径
- for (const item of seq) {
- const startX = Math.round(x);
- // 垂直居中:让整行文字在窗口高度居中
- const baseY = (H - item.h * cell + CHAR_GAP) / 2;
- for (let row = 0; row < item.h; row++) {
- for (let col = 0; col < item.w; col++) {
- if (item.on[row][col]) {
- const cx = startX + col * cell + dot / 2;
- const cy = baseY + row * cell + dot / 2;
- drawLedDot(cx, cy, r, LED_COLOR);
- }
- }
- }
- x += item.w * cell + GAP * dot;
- }
- }
- function renderFrame(ts) {
- if (!lastTs) lastTs = ts;
- const dt = (ts - lastTs) / 1000;
- lastTs = ts;
- // 统一用 CSS 像素坐标(ctx 已 setTransform(dpr),画布物理尺寸=CSS*dpr)
- const W = canvas.clientWidth, H = canvas.clientHeight;
- ctx.clearRect(0, 0, W, H);
- if (seq.length > 0 && W > 0 && H > 0) {
- const dot = dotSize(H);
- const totalW = totalPxWidth(dot); // 整行像素宽
- const sep = Math.max(dot * 6, 80); // 两遍文字间的最小间距
- const period = Math.max(totalW + sep, W + sep); // 循环周期:至少覆盖一个窗口宽,保证无缝
- // 向左滚动,跨过整段周期后回绕,实现连续滚动
- offsetX -= speed * dt;
- if (offsetX <= -period) offsetX += period;
- // 绘制两遍:第一遍 offsetX,第二遍 offsetX+period,窗口内始终有内容,无缝衔接
- drawText(offsetX, H, dot);
- drawText(offsetX + period, H, dot);
- }
- requestAnimationFrame(renderFrame);
- }
- // 初始化:读取文字、颜色与速度、布局、设置画布尺寸
- async function init() {
- console.log("LED_NEW_MARKER_2026"); // 验证 vite 是否读取新代码
- // 页面已加载完成 => WebView2 控制器必然就绪,通知后端补设透明背景
- // (Wails 创建窗口时设置的透明可能因控制器异步未就绪而丢失)
- SettingsService.SetLEDTransparent();
- const [text, color, spd] = await Promise.all([
- SettingsService.GetLEDText(),
- SettingsService.GetLEDColor(),
- SettingsService.GetLEDSpeed(),
- ]);
- LED_COLOR = color || '#ff4d4d';
- if (spd > 0) speed = spd;
- seq = textToSequence(text || 'LED 滚动文字');
- maxRows = layout(seq, GAP).maxH;
- dpr = window.devicePixelRatio || 1;
- // 设置画布物理尺寸并重置变换(赋值会清空矩阵,需每次重设)
- const setupCanvas = () => {
- canvas.width = Math.floor(canvas.clientWidth * dpr);
- canvas.height = Math.floor(canvas.clientHeight * dpr);
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // 之后所有绘制用 CSS 像素
- };
- setupCanvas();
- // 窗口尺寸变化时重设画布(同样重设变换,避免 scale 丢失)
- new ResizeObserver(setupCanvas).observe(canvas);
- requestAnimationFrame(renderFrame);
- }
- // 关闭按钮
- document.getElementById('led-close').addEventListener('click', () => {
- SettingsService.CloseLEDWindow();
- });
- init();
|