Files
APP/components/TextToHex/textToDotMatrixFor7305.vue
2025-11-05 19:18:05 +08:00

331 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view>
<canvas canvas-id="reusableCanvas" :width="currentCanvasWidth" :height="currentCanvasHeight"
class="offscreen-canvas"></canvas>
</view>
</template>
<script>
export default {
name: "textToDotMatrixFor7305",
props: {
txts: {
type: Array,
default: () => [],
validator: (value) => value.every(item => typeof item === 'string')
},
fontSize: {
type: Number,
default: 16,
validator: (value) => value > 0 && value <= 100
},
bgColor: {
type: String,
default: "#ffffff"
},
color: {
type: String,
default: "#000000"
},
// 二值化阈值0~1越大越细默认偏清晰
threshold: {
type: Number,
default: 0.45
},
// 是否启用轻度笔画修复3x3 膨胀);默认关闭以避免变粗
sharpen: {
type: Boolean,
default: false
}
},
data() {
return {
// 当前Canvas的宽高动态调整
currentCanvasWidth: 0,
currentCanvasHeight: 0,
// Canvas上下文复用
ctx: null
};
},
computed: {
validTxts() {
return this.txts.filter(line => line.trim() !== '');
}
},
mounted() {
// 初始化Canvas上下文只创建一次
this.ctx = uni.createCanvasContext('reusableCanvas', this);
},
methods: {
/**
* 外部可调用:复位画布为纯背景并立即提交
*/
async resetCanvas() {
if (!this.ctx) return;
this.clearCanvas();
await new Promise((resolve) => {
this.ctx.draw(true, () => setTimeout(resolve, 30));
});
},
/**
* 估算单行文本所需的Canvas宽度
*/
calcLineWidth(textLine) {
return textLine.length * this.fontSize;
},
/**
* 清除Canvas内容
*/
clearCanvas() {
// 先清除,再用背景色填充,确保无残留
this.ctx.clearRect(0, 0, this.currentCanvasWidth, this.currentCanvasHeight);
this.ctx.setFillStyle(this.bgColor);
this.ctx.fillRect(0, 0, this.currentCanvasWidth, this.currentCanvasHeight);
},
/**
* 复用单个Canvas处理所有文本行
*/
async drawAndGetPixels() {
// 发送前:确保画布处于干净背景态
await this.resetCanvas();
// 超采样比例(提高分辨率再降采样,减少模糊)
const SCALE = 3;
const PADDING_X = 1 * SCALE; // 左侧预留像素,避免首字裁剪
let binaryToHex = (binaryArray) => {
if (!Array.isArray(binaryArray) || binaryArray.length !== 8) {
throw new Error("输入必须是包含8个元素的二进制数组");
}
// 检查每个元素是否为0或1
if (!binaryArray.every(bit => bit === 0 || bit === 1)) {
throw new Error("数组元素必须只能是0或1");
}
// 将二进制数组转换为十进制数
let decimalValue = 0;
for (let i = 0; i < 8; i++) {
// 计算每个位的权重并累加
decimalValue += binaryArray[i] * Math.pow(2, 7 - i);
}
// 将十进制转换为十六进制字符串并添加0x前缀
const hexString = "0x" + decimalValue.toString(16).padStart(2, '0').toUpperCase();
return hexString;
}
let convertCharToMatrix = (drawResult, item) => {
// 设备端使用13x13点阵渲染保持输出13列×13行但只在内部采样12×12保留右侧与底部1像素缓冲
const charWidth = 13;
const charHeight = 13;
const effectiveWidth = 12; // 仅采样前12列
const effectiveHeight = 12; // 仅采样前12行
const { pixelData, width, height } = drawResult;
// 将高分辨率像素降采样为13x13的布尔矩阵
const target = new Array(charWidth * charHeight).fill(0);
const threshold = Math.max(0.2, Math.min(0.8, this.threshold || 0.45));
// 确保采样区域严格对齐从PADDING_X开始只采样12列右侧预留1px12行底部预留1px
// 字符实际绘制区域从PADDING_X开始宽度为12*SCALE
const charStartX = PADDING_X;
const charEndX = PADDING_X + effectiveWidth * SCALE; // 仅采样到第12列
for (let y = 0; y < charHeight; y++) {
for (let x = 0; x < charWidth; x++) {
// 超出有效采样区域第13列或第13行直接置0作为缓冲
if (x >= effectiveWidth || y >= effectiveHeight) {
target[y * charWidth + x] = 0;
continue;
}
let onCount = 0;
let total = 0;
// 采样区域字符从PADDING_X开始绘制每列宽度为SCALE
const startX = PADDING_X + x * SCALE;
const startY = y * SCALE;
for (let sy = 0; sy < SCALE; sy++) {
for (let sx = 0; sx < SCALE; sx++) {
const px = startX + sx;
const py = startY + sy;
// 边界检查确保不超出Canvas边界且不超出字符实际绘制区域
if (px < 0 || py < 0 || px >= width || py >= height) {
// 边界外视为背景(黑色)
continue;
}
// 额外检查:确保不采样到字符右侧的残留区域
if (px >= charEndX) {
// 超出字符区域,视为背景
continue;
}
const idx = (py * width + px) * 4;
const R = pixelData[idx];
const G = pixelData[idx + 1];
const B = pixelData[idx + 2];
const A = pixelData[idx + 3] || 255;
// 计算亮度
const luminance = 0.299 * R + 0.587 * G + 0.114 * B;
const alpha = A / 255;
// 背景是黑色,文字是白色,所以判断亮色(>=128为文字点
if (luminance >= 128 && alpha > 0.5) onCount++;
total++;
}
}
// 当亮色占比超过阈值时判为1至少需要采样到一些像素
target[y * charWidth + x] = (total > 0 && onCount / total >= threshold) ? 1 : 0;
}
}
// 轻度笔画修复:可选 3x3 膨胀(启用时阈值更严格,避免变粗)
if (this.sharpen) {
const dilated = target.slice();
for (let y = 0; y < effectiveHeight; y++) {
for (let x = 0; x < effectiveWidth; x++) {
const idx = y * charWidth + x;
if (target[idx] === 1) continue;
let neighbors = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || ny < 0 || nx >= effectiveWidth || ny >= effectiveHeight) continue;
if (target[ny * charWidth + nx] === 1) neighbors++;
}
}
// 使用更严格的邻居门限,避免整体变粗
if (neighbors >= 5) {
dilated[idx] = 1;
}
}
}
for (let i = 0; i < target.length; i++) target[i] = dilated[i];
}
const lowBytes = new Array(charWidth).fill(0);
const highBytes = new Array(charWidth).fill(0);
// 按列打包每列13行分成上下两字节低字节0-7行8行高字节8-12行5行
for (let col = 0; col < charWidth; col++) {
for (let row = 0; row < charHeight; row++) {
const pixel = target[row * charWidth + col];
if (pixel === 1) {
if (row < 8) {
// 低字节0-7行从低位到高位
lowBytes[col] |= (1 << row);
} else {
// 高字节8-12行从低位到高位使用5位剩余3位未使用
highBytes[col] |= (1 << (row - 8));
}
}
}
}
return [...lowBytes, ...highBytes];
}
let drawTxt = async (textLine) => {
let result = {};
let ctx = this.ctx;
// 1. 动态调整Canvas尺寸高分辨率
// 设备端使用13x13点阵渲染
this.currentCanvasWidth = 13 * SCALE + PADDING_X;
this.currentCanvasHeight = 13 * SCALE;
// 2. 清空Canvas绘制背景
this.clearCanvas();
// 3. 设置文字样式(整数像素对齐,顶部基线,避免首字裁剪)
ctx.setFillStyle(this.color);
ctx.setTextBaseline('top');
ctx.setTextAlign('left');
const fs = Math.max(1, Math.round(this.fontSize)) * SCALE;
ctx.setFontSize(fs);
ctx.font = `${fs}px "PingFangBold", "PingFang SC", Arial, sans-serif`;
// 4. 绘制单个字符(每个字符独立绘制在固定位置)
// 确保字符始终从PADDING_X开始绘制Y坐标为0保证采样一致性
const charX = PADDING_X;
const charY = 0;
ctx.fillText(textLine, charX, charY);
// 5. 异步绘制并获取像素数据(串行处理避免冲突)
const grabPixels = () => new Promise((resolve, reject) => {
// 立即绘制并给一点缓冲时间,避免取像素过早
ctx.draw(true, () => {
setTimeout(() => {
uni.canvasGetImageData({
canvasId: 'reusableCanvas',
x: 0,
y: 0,
width: this.currentCanvasWidth,
height: this.currentCanvasHeight,
success: res => {
result = {
line: textLine,
pixelData: res.data,
width: this.currentCanvasWidth,
height: this.currentCanvasHeight
};
resolve();
},
fail: err => reject(err)
});
}, 70);
});
});
await grabPixels();
// 一次性校验:若像素全黑或明显异常,重绘重取一次
let nonZero = false;
for (let i = 0; i < result.pixelData.length; i += 4) {
if (result.pixelData[i] || result.pixelData[i+1] || result.pixelData[i+2]) { nonZero = true; break; }
}
if (!nonZero) {
await new Promise(r => setTimeout(r, 50));
await grabPixels();
}
return result;
}
let arr = [];
// 循环处理每行文本
for (let i = 0; i < this.validTxts.length; i++) {
let linePixls = [];
let item = this.validTxts[i];
// console.log("item=", item);
for (var j = 0; j < item.length; j++) {
let char = item[j];
let result = await drawTxt(char);
let matrix = convertCharToMatrix(result, item);
// 调试:打印每个字符的点阵数据
console.log(`[点阵生成] 字符"${char}" 点阵数据:`, matrix.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' '));
linePixls.push(matrix);
// 在字符间增加轻微延时,避免相邻提取竞争(尤其是末字符)
await new Promise(r => setTimeout(r, 20));
}
// console.log("hexs=", linePixls.join(","));
arr.push(linePixls);
// 每行结束再等一会,提高末字符稳定性
await new Promise(r => setTimeout(r, 40));
}
// 发送后:再次清空画布,避免残留影响下一次
await this.resetCanvas();
return arr;
}
}
};
</script>
<style>
.offscreen-canvas {
position: fixed;
left: -9999px;
top: -9999px;
}
</style>