// ==UserScript== // @name Steam字符画生成器 v2.1 // @namespace https://steamcommunity.com/ // @version 2.1 // @description 图片转Unicode字符画;按UTF-8字节统计,适配Steam 1000字节留言限制 // @author Victor幻月 // @match *://steamcommunity.com/* // @match *://store.steampowered.com/* // @grant GM_xmlhttpRequest // @connect * // ==/UserScript== (function () { 'use strict'; // ==================== 配置 ==================== var CURATOR_URL = 'https://store.steampowered.com/curator/32969453/'; var STEAM_BYTE_LIMIT = 1000; // ==================== 字符常量 ==================== var BRAILLE_MAP = { '0,0': 0x01, '0,1': 0x02, '0,2': 0x04, '1,0': 0x08, '1,1': 0x10, '1,2': 0x20, '0,3': 0x40, '1,3': 0x80 }; var BAYER_2x4 = [[0, 4], [6, 2], [1, 5], [7, 3]]; function utf8ByteLength(str) { try { return new TextEncoder().encode(str).length; } catch (e) { return str.length * 3; } } // ==================== 图像算法 ==================== function gaussianBlur3(src, w, h) { var k = [1, 2, 1]; var tmp = new Float32Array(w * h); for (var y = 0; y < h; y++) { for (var x = 0; x < w; x++) { var s = 0, sum = 0; for (var i = -1; i <= 1; i++) { var xx = x + i; if (xx < 0 || xx >= w) continue; s += src[y * w + xx] * k[i + 1]; sum += k[i + 1]; } tmp[y * w + x] = s / sum; } } var out = new Float32Array(w * h); for (var y2 = 0; y2 < h; y2++) { for (var x2 = 0; x2 < w; x2++) { var s2 = 0, sum2 = 0; for (var j = -1; j <= 1; j++) { var yy = y2 + j; if (yy < 0 || yy >= h) continue; s2 += tmp[yy * w + x2] * k[j + 1]; sum2 += k[j + 1]; } out[y2 * w + x2] = s2 / sum2; } } return out; } function preprocess(gray, w, h, o) { var g = Float32Array.from(gray); if (o.denoise > 0) { var blurred = gaussianBlur3(g, w, h); var t = Math.min(1, o.denoise); for (var i = 0; i < g.length; i++) g[i] = g[i] * (1 - t) + blurred[i] * t; } var c = o.contrast, b = o.brightness, gm = o.gamma; for (var i2 = 0; i2 < g.length; i2++) { var v = (g[i2] - 128) * c + 128 + b; v = v < 0 ? 0 : v > 255 ? 255 : v; if (gm !== 1) v = 255 * Math.pow(v / 255, gm); g[i2] = v; } if (o.sharpen > 0) { var blurred2 = gaussianBlur3(g, w, h); for (var i3 = 0; i3 < g.length; i3++) { var v2 = g[i3] + (g[i3] - blurred2[i3]) * o.sharpen; g[i3] = v2 < 0 ? 0 : v2 > 255 ? 255 : v2; } } return g; } function adaptiveThreshold(gray, w, h, blockRatio, offset) { var integral = new Float64Array((w + 1) * (h + 1)); for (var y = 0; y < h; y++) { var rowSum = 0; for (var x = 0; x < w; x++) { rowSum += gray[y * w + x]; integral[(y + 1) * (w + 1) + (x + 1)] = integral[y * (w + 1) + (x + 1)] + rowSum; } } var bw = Math.max(1, Math.round(w * blockRatio)); var bh = Math.max(1, Math.round(h * blockRatio)); var bin = new Uint8Array(w * h); var W1 = w + 1; for (var y2 = 0; y2 < h; y2++) { var y1 = y2 - bh < 0 ? 0 : y2 - bh; var y3 = y2 + bh >= h ? h - 1 : y2 + bh; for (var x2 = 0; x2 < w; x2++) { var x1 = x2 - bw < 0 ? 0 : x2 - bw; var x3 = x2 + bw >= w ? w - 1 : x2 + bw; var area = (x3 - x1 + 1) * (y3 - y1 + 1); var sum = integral[(y3 + 1) * W1 + (x3 + 1)] - integral[y1 * W1 + (x3 + 1)] - integral[(y3 + 1) * W1 + x1] + integral[y1 * W1 + x1]; var mean = sum / area; bin[y2 * w + x2] = gray[y2 * w + x2] < mean * (1 - offset) ? 0 : 255; } } return bin; } function simpleThreshold(gray, t) { var bin = new Uint8Array(gray.length); for (var i = 0; i < gray.length; i++) bin[i] = gray[i] < t ? 0 : 255; return bin; } function fsDither(gray, w, h, t) { var work = Float32Array.from(gray); var bin = new Uint8Array(w * h); for (var y = 0; y < h; y++) { for (var x = 0; x < w; x++) { var i = y * w + x; var oldV = work[i]; var newV = oldV < t ? 0 : 255; bin[i] = newV; var err = oldV - newV; if (x + 1 < w) work[i + 1] += err * 7 / 16; if (y + 1 < h) { if (x > 0) work[i + w - 1] += err * 3 / 16; work[i + w] += err * 5 / 16; if (x + 1 < w) work[i + w + 1] += err * 1 / 16; } } } return bin; } function atkinsonDither(gray, w, h, t) { var work = Float32Array.from(gray); var bin = new Uint8Array(w * h); for (var y = 0; y < h; y++) { for (var x = 0; x < w; x++) { var i = y * w + x; var oldV = work[i]; var newV = oldV < t ? 0 : 255; bin[i] = newV; var err = (oldV - newV) / 8; if (x + 1 < w) work[i + 1] += err; if (x + 2 < w) work[i + 2] += err; if (y + 1 < h) { if (x > 0) work[i + w - 1] += err; work[i + w] += err; if (x + 1 < w) work[i + w + 1] += err; } if (y + 2 < h) work[i + 2 * w] += err; } } return bin; } function invertBin(bin) { for (var i = 0; i < bin.length; i++) bin[i] = 255 - bin[i]; } function blockHalftone(gray, w, h, invert) { var cols = Math.floor(w / 2); var rows = Math.floor(h / 4); var lines = []; for (var row = 0; row < rows; row++) { var line = ''; for (var col = 0; col < cols; col++) { var sum = 0; for (var dy = 0; dy < 4; dy++) for (var dx = 0; dx < 2; dx++) sum += gray[(row * 4 + dy) * w + (col * 2 + dx)]; var avg = sum / 8; var n = Math.round((255 - avg) / 255 * 8); if (invert) n = 8 - n; var code = 0; for (var dy2 = 0; dy2 < 4; dy2++) for (var dx2 = 0; dx2 < 2; dx2++) if (BAYER_2x4[dy2][dx2] < n) code |= BRAILLE_MAP[dx2 + ',' + dy2]; line += String.fromCharCode(0x2800 + code); } lines.push(line); } return lines.join('\n'); } function binaryToBraille(bin, w, h) { var cols = Math.floor(w / 2); var rows = Math.floor(h / 4); var lines = []; for (var row = 0; row < rows; row++) { var line = ''; for (var col = 0; col < cols; col++) { var code = 0; for (var dy = 0; dy < 4; dy++) for (var dx = 0; dx < 2; dx++) if (bin[(row * 4 + dy) * w + (col * 2 + dx)] === 0) code |= BRAILLE_MAP[dx + ',' + dy]; line += String.fromCharCode(0x2800 + code); } lines.push(line); } return lines.join('\n'); } function imageToBraille(img, o) { var pixelWidth = o.width * 2; var pixelHeight = Math.round(pixelWidth * (img.naturalHeight / img.naturalWidth) * o.heightScale); pixelHeight = Math.max(4, Math.ceil(pixelHeight / 4) * 4); var canvas = document.createElement('canvas'); canvas.width = pixelWidth; canvas.height = pixelHeight; var ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; var z = o.zoom; var sw = img.naturalWidth / z; var sh = img.naturalHeight / z; var sx = (img.naturalWidth - sw) / 2; var sy = (img.naturalHeight - sh) / 2; ctx.drawImage(img, sx, sy, sw, sh, 0, 0, pixelWidth, pixelHeight); var data = ctx.getImageData(0, 0, pixelWidth, pixelHeight).data; var gray = new Float32Array(pixelWidth * pixelHeight); for (var i = 0; i < gray.length; i++) { var p = i * 4; gray[i] = 0.299 * data[p] + 0.587 * data[p + 1] + 0.114 * data[p + 2]; } var g = preprocess(gray, pixelWidth, pixelHeight, o); if (o.mode === 'halftone') { return blockHalftone(g, pixelWidth, pixelHeight, o.invert); } var bin; if (o.mode === 'adaptive') bin = adaptiveThreshold(g, pixelWidth, pixelHeight, o.blockRatio, o.offset); else if (o.mode === 'fs') bin = fsDither(g, pixelWidth, pixelHeight, o.threshold); else if (o.mode === 'atkinson') bin = atkinsonDither(g, pixelWidth, pixelHeight, o.threshold); else bin = simpleThreshold(g, o.threshold); if (o.invert) invertBin(bin); return binaryToBraille(bin, pixelWidth, pixelHeight); } // ==================== UI 样式 ==================== var style = document.createElement('style'); style.textContent = [ '#bap-panel{position:fixed;bottom:20px;right:20px;z-index:99999;width:360px;', 'background:#1b2838;color:#c6d4df;border:1px solid #66c0f4;border-radius:6px;', 'font-family:Arial,sans-serif;font-size:13px;', 'box-shadow:0 4px 12px rgba(0,0,0,.5);overflow:hidden}', '#bap-header{padding:8px 12px;background:#2a475e;color:#fff;font-weight:bold;', 'cursor:pointer;user-select:none;display:flex;justify-content:space-between;align-items:center}', '#bap-header .badge{font-size:11px;color:#8bc34a;font-weight:normal}', '#bap-body{padding:12px;display:none;max-height:85vh;overflow-y:auto}', '#bap-suggest{padding:8px 12px;background:#2a475e;border-left:3px solid #ffc107;', 'font-size:11px;color:#c6d4df;display:flex;justify-content:space-between;align-items:center;gap:8px}', '#bap-suggest a{color:#66c0f4;text-decoration:underline}', '#bap-suggest .close{background:none;border:none;color:#8f98a0;cursor:pointer;font-size:14px;padding:0 4px}', '#bap-panel .row{display:flex;align-items:center;gap:6px;margin-bottom:6px}', '#bap-panel label.lb{font-size:12px;min-width:66px;flex-shrink:0}', '#bap-panel input[type=range]{flex:1;min-width:0}', '#bap-panel .val{font-size:11px;min-width:30px;text-align:right;color:#8f98a0}', '#bap-panel select,#bap-panel input[type=text],#bap-panel input[type=file]{', 'width:100%;box-sizing:border-box;padding:4px;background:#16202d;', 'border:1px solid #4c6b8a;color:#c6d4df;border-radius:3px;font-size:12px}', '#bap-panel select:disabled{opacity:.4}', '#bap-panel input[type=range]:disabled{opacity:.4}', '#bap-panel .btn{padding:6px;border:none;border-radius:3px;cursor:pointer;', 'font-weight:bold;font-size:12px}', '#bap-out{width:100%;height:190px;box-sizing:border-box;background:#16202d;', 'border:1px solid #4c6b8a;color:#c6d4df;white-space:pre;overflow:auto;', 'padding:4px;resize:vertical;font-family:Consolas,Courier New,monospace}', '#bap-panel details{margin:6px 0;border-top:1px solid #2a475e;padding-top:6px}', '#bap-panel summary{cursor:pointer;font-size:12px;color:#66c0f4;margin-bottom:6px}', '#bap-panel .hint{font-size:10px;color:#556677;margin-top:4px}', '#bap-panel .status{font-size:11px;color:#8f98a0;margin:4px 0;min-height:14px}', '#bap-panel .status.ok{color:#8bc34a}', '#bap-panel .status.warn{color:#ffc107}', '#bap-panel .status.err{color:#ff6b6b}', '#bap-panel .btn-primary{background:#66c0f4;color:#1b2838}', '#bap-panel .btn-secondary{background:#4c6b8a;color:#fff}' ].join(''); document.head.appendChild(style); // ==================== 面板骨架 ==================== var panel = document.createElement('div'); panel.id = 'bap-panel'; panel.innerHTML = [ '