墨水屏蓝牙控制器上位机学习(1)-ELink墨水屏电子纸社区-FPGA CPLD-ChipDebug

墨水屏蓝牙控制器上位机学习(1)

 

大佬tsl0922上位机界面如下 :

图片[1]-墨水屏蓝牙控制器上位机学习(1)-ELink墨水屏电子纸社区-FPGA CPLD-ChipDebug

main.css文件如下 :

:root {     --primary-color: #0d6efd;     --primary-hover: #0b5ed7;     --secondary-color: #6c757d;     --secondary-hover: #5c636a;      --dark-bg: #121212;     --dark-text: #e0e0e0;     --dark-fieldset-bg: #1e1e1e;     --dark-border: #333;     --dark-code-bg: #2d2d2d;     --dark-log-bg: #2a2a2a;     --dark-input-bg: #2d2d2d;     --dark-input-text: #e0e0e0; }  body {     margin: 0;     padding: 0;     font-family: system-ui, -apple-system, sans-serif; }  .debug {     display: none !important; }  body.debug-mode .debug {     display: flex !important; }  body.debug-mode {     background-color: var(--dark-bg);     color: var(--dark-text); }  body.debug-mode .main {     background-color: var(--dark-bg);     color: var(--dark-text); }  body.debug-mode fieldset {     background-color: var(--dark-fieldset-bg);     box-shadow: 0 .5rem 0.5rem rgba(0, 0, 0, 0.5); }  body.debug-mode h3 {     border-bottom: 1px solid var(--dark-border);     color: var(--dark-text); }  body.debug-mode code {     background: var(--dark-code-bg);     color: #ff9800; }  body.debug-mode #log {     background: var(--dark-log-bg);     border: 1px solid var(--dark-border); }  body.debug-mode #log .time {     color: #8bc34a; }  body.debug-mode #log .action {     color: #03a9f4; }  body.debug-mode input[type=text], body.debug-mode input[type=number], body.debug-mode select {     background-color: var(--dark-input-bg);     color: var(--dark-input-text);     border-color: var(--dark-border); }  body.debug-mode input[type=file] {     color: var(--dark-input-text);     background-color: transparent;     border-color: var(--dark-border); }  body.debug-mode input[type=file]::file-selector-button {     background-color: var(--dark-fieldset-bg);     color: var(--dark-input-text);     border-color: var(--dark-border); }  body.debug-mode input[type=file]::file-selector-button:hover {     background-color: #333;     border-color: #444; }  body.debug-mode fieldset legend {     color: #64b5f6; }  .main {     width: 100%;     max-width: 950px;     margin: 0 auto;     padding: 0 1rem;     background: #fff;     font-size: 1rem;     font-weight: 400;     line-height: 1.5;     box-sizing: border-box; }  .footer {     display: flex;     gap: 10px;     font-size: 0.8rem;     color: #666;     flex-wrap: wrap;     margin: 1rem 0; }  .footer .links {     display: flex;     align-items: center; }  .footer .links a {     color: #666;     text-decoration: none;     position: relative;     padding: 0 8px; }  .footer .links a:first-child {     padding-left: 0; }  .footer .links a:not(:last-child)::after {     content: "•";     position: absolute;     right: -4px;     color: #999; }  .footer a:hover {     color: #0d6efd;     text-decoration: underline; }  body.debug-mode .footer .links a:not(:last-child)::after {     color: #666; }  body.debug-mode .footer {     color: #999; }  body.debug-mode .footer a {     color: #999; }  body.debug-mode .footer a:hover {     color: #64b5f6; }  h3 {     padding-bottom: .3em;     border-bottom: 1px solid #CCC;     text-align: center; }  fieldset {     border: none;     box-shadow: 0 .5rem 0.5rem rgba(0, 0, 0, 0.2);     background-color: #f8f9fa;     padding: 10px;     margin-bottom: 16px;     border-radius: 4px; }  fieldset legend {     font-weight: bold;     color: rgba(0, 0, 255, 0.6); }  code {     padding: .2em .4em;     margin: 0;     font-size: 85%;     background: #CCC;     border-radius: 3px; }  .flex-container {     display: flex;     flex-wrap: wrap;     gap: 8px;     margin-bottom: 8px; }  .flex-group {     display: flex;     flex-wrap: wrap;     gap: 8px;     align-items: center; }  #status {     margin: 10px 0; }  #log {     width: 100%;     min-height: 100px;     max-height: 300px;     margin: 0;     padding: 5px;     background: #DDD;     overflow-y: auto;     overflow-x: hidden;     font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;     box-sizing: border-box;     word-break: break-word; }  #log div {     padding: 2px 0; }  #log .time, #log .action {     display: inline-block;     white-space: nowrap; }  #log .time {     color: #333;     margin-right: 0.5em; }  #log .action {     color: #666;     margin-right: 0.5em; }  #canvas-box {     margin-top: 10px;     width: 100%; }  #canvas {     border: black solid 1px;     max-width: 100%;     height: auto;     display: block;     margin: 0 auto; }  button {     padding: 0.375rem 0.75rem;     border: 1px solid var(--primary-color);     border-radius: 0.375rem;     margin-bottom: 5px;     white-space: nowrap;     cursor: pointer;     font-size: 0.9rem; }  button:disabled {     opacity: 0.65; }  button.primary {     color: #fff;     background-color: var(--primary-color); }  button.primary:hover {     color: #fff;     border-color: var(--primary-hover);     background-color: var(--primary-hover); }  button.secondary {     color: #fff;     background-color: var(--secondary-color);     border-color: var(--secondary-color); }  button.secondary:hover {     color: #fff;     border-color: var(--secondary-hover);     background-color: var(--secondary-hover); }  input[type=text], input[type=number], select {     font-size: 1rem;     font-weight: 400;     line-height: 1.5;     color: #212529;     border: 1px solid #dee2e6;     border-radius: 0.375rem;     padding: .2rem .75rem;     max-width: 100%;     box-sizing: border-box; }  input[type=file] {     font-size: 1rem;     font-weight: 400;     line-height: 1.5;     color: #212529;     max-width: 100%; }  input::file-selector-button {     font-size: 0.9rem;     font-weight: 400;     line-height: 1.5;     border: 1px solid var(--primary-color);     border-radius: 0.375rem;     cursor: pointer; }  select {     padding: .3rem 2.25rem .3rem .75rem; }  input:focus, select:focus {     border: 1px solid #86b7fe;     box-shadow: 0 0 4px rgba(0, 120, 215, 0.8);     outline: 0; }  label {     margin-right: 4px;     white-space: nowrap; }  .status-bar {     display: none;     font-size: 85%;     color: #666;     margin-bottom: 10px;     padding-bottom: 10px;     border-bottom: 1px dotted #AAA; }  canvas.text-placement-mode {     border: 2px dashed var(--primary-color) !important;     cursor: text !important; /* Force text cursor */ }  .canvas-title {     display: none;     text-align: center;     margin-bottom: 5px;     color: var(--primary-color); }  .canvas-tools {     margin-top: 10px;     justify-content: center; }  .text-tools {     display: none; }  .tool-button {     width: 36px;     height: 36px;     font-size: 1.2rem;     display: inline-flex;     align-items: center;     justify-content: center;     margin-right: 5px;     background-color: #f8f9fa;     border: 1px solid #dee2e6;     border-radius: 4px;     padding: 0;     cursor: pointer;     transition: all 0.2s ease; }  .tool-button:hover {     background-color: #e9ecef;     border-color: #ced4da; }  .tool-button.active {     background-color: var(--primary-color);     color: white;     border-color: var(--primary-color); }  body.debug-mode .tool-button {     background-color: var(--dark-input-bg);     border-color: var(--dark-border);     color: var(--dark-text); }  body.debug-mode .tool-button:hover {     background-color: #3a3a3a;     border-color: #444; }  body.debug-mode .tool-button.active {     background-color: var(--primary-color);     color: white;     border-color: var(--primary-hover); }  input[type=text]:disabled, input[type=number]:disabled, select:disabled {     opacity: 0.65;     cursor: not-allowed;     background-color: #e9ecef;     color: #6c757d; }  body.debug-mode input[type=text]:disabled, body.debug-mode input[type=number]:disabled, body.debug-mode select:disabled {     background-color: #1a1a1a;     color: #666;     border-color: #2a2a2a; }  @media (max-width: 768px) {     .flex-container {         flex-direction: column;     }     .flex-container.options .flex-group label {         min-width: 80px;     }      .canvas-tools.flex-container {         flex-direction: row;         flex-wrap: wrap;         justify-content: center;     }      .canvas-tools .flex-group {         justify-content: center;         width: 100%;     }      #log {         height: 150px;         margin-top: 10px;     }      fieldset {         padding: 8px;     }      button {         width: auto;     }      input[type=text],     input[type=number],     select {         max-width: 100%;         margin-bottom: 5px;     } }

三个js文件分别如下 :

dithering.js

// Ported from: https://e-paper-display.cn/usb2epd.html  // 固定的六色调色板 const rgbPalette = [   { name: "黄色", r: 255, g: 255, b: 0, value: 0xe2 },   { name: "绿色", r: 41, g: 204, b: 20, value: 0x96 },   { name: "蓝色", r: 0, g: 0, b: 255, value: 0x1d },   { name: "红色", r: 255, g: 0, b: 0, value: 0x4c },   { name: "黑色", r: 0, g: 0, b: 0, value: 0x00 },   { name: "白色", r: 255, g: 255, b: 255, value: 0xff } ];  // 四色调色板 const fourColorPalette = [   { name: "黑色", r: 0, g: 0, b: 0, value: 0x00 },   { name: "白色", r: 255, g: 255, b: 255, value: 0x01 },   { name: "红色", r: 255, g: 0, b: 0, value: 0x03 },   { name: "黄色", r: 255, g: 255, b: 0, value: 0x02 } ];  // 三色调色板 const threeColorPalette = [   { name: "黑色", r: 0, g: 0, b: 0, value: 0x00 },   { name: "白色", r: 255, g: 255, b: 255, value: 0x01 },   { name: "红色", r: 255, g: 0, b: 0, value: 0x02 } ];  function adjustContrast(imageData, factor) {   const data = imageData.data;   for (let i = 0; i < data.length; i += 4) {     data[i] = Math.min(255, Math.max(0, (data[i] - 128) * factor + 128));     data[i + 1] = Math.min(255, Math.max(0, (data[i + 1] - 128) * factor + 128));     data[i + 2] = Math.min(255, Math.max(0, (data[i + 2] - 128) * factor + 128));   }   return imageData; }  function rgbToLab(r, g, b) {   r = r / 255;   g = g / 255;   b = b / 255;    r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;   g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;   b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;    r *= 100;   g *= 100;   b *= 100;    let x = r * 0.4124 + g * 0.3576 + b * 0.1805;   let y = r * 0.2126 + g * 0.7152 + b * 0.0722;   let z = r * 0.0193 + g * 0.1192 + b * 0.9505;    x /= 95.047;   y /= 100.0;   z /= 108.883;    x = x > 0.008856 ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116);   y = y > 0.008856 ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116);   z = z > 0.008856 ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116);    const l = (116 * y) - 16;   const a = 500 * (x - y);   const bLab = 200 * (y - z);    return { l, a, b: bLab }; }  function labDistance(lab1, lab2) {   const dl = lab1.l - lab2.l;   const da = lab1.a - lab2.a;   const db = lab1.b - lab2.b;   return Math.sqrt(0.2 * dl * dl + 3 * da * da + 3 * db * db); }  function findClosestColor(r, g, b) {   const mode = document.getElementById('ditherMode').value;   let palette;    if (mode === 'fourColor') {     palette = fourColorPalette;   } else if (mode === 'threeColor') {     palette = threeColorPalette;   } else {     palette = rgbPalette;   }    // 蓝色特殊情况(仅限非三色、四色模式)   if (mode !== 'fourColor' && mode !== 'threeColor' && r < 50 && g < 150 && b > 100) {     return rgbPalette[2]; // 蓝色   }    // 三色模式下优先检测红色   if (mode === 'threeColor') {     // 如果红色通道显著高于绿色和蓝色,且强度足够     if (r > 120 && r > g * 1.5 && r > b * 1.5) {       return threeColorPalette[2]; // 红色     }     // 否则根据亮度选择黑或白     const luminance = 0.299 * r + 0.587 * g + 0.114 * b;     return luminance < 128 ? threeColorPalette[0] : threeColorPalette[1]; // 黑色或白色   }    const inputLab = rgbToLab(r, g, b);   let minDistance = Infinity;   let closestColor = palette[0];    for (const color of palette) {     const colorLab = rgbToLab(color.r, color.g, color.b);     const distance = labDistance(inputLab, colorLab);     if (distance < minDistance) {       minDistance = distance;       closestColor = color;     }   }    return closestColor; }  function floydSteinbergDither(imageData, strength) {   const width = imageData.width;   const height = imageData.height;   const data = imageData.data;   const tempData = new Uint8ClampedArray(data);    for (let y = 0; y < height; y++) {     for (let x = 0; x < width; x++) {       const idx = (y * width + x) * 4;       const r = tempData[idx];       const g = tempData[idx + 1];       const b = tempData[idx + 2];        const closest = findClosestColor(r, g, b);        const errR = (r - closest.r) * strength;       const errG = (g - closest.g) * strength;       const errB = (b - closest.b) * strength;        if (x + 1 < width) {         const idxRight = idx + 4;         tempData[idxRight] = Math.min(255, Math.max(0, tempData[idxRight] + errR * 7 / 16));         tempData[idxRight + 1] = Math.min(255, Math.max(0, tempData[idxRight + 1] + errG * 7 / 16));         tempData[idxRight + 2] = Math.min(255, Math.max(0, tempData[idxRight + 2] + errB * 7 / 16));       }       if (y + 1 < height) {         if (x > 0) {           const idxDownLeft = idx + width * 4 - 4;           tempData[idxDownLeft] = Math.min(255, Math.max(0, tempData[idxDownLeft] + errR * 3 / 16));           tempData[idxDownLeft + 1] = Math.min(255, Math.max(0, tempData[idxDownLeft + 1] + errG * 3 / 16));           tempData[idxDownLeft + 2] = Math.min(255, Math.max(0, tempData[idxDownLeft + 2] + errB * 3 / 16));         }         const idxDown = idx + width * 4;         tempData[idxDown] = Math.min(255, Math.max(0, tempData[idxDown] + errR * 5 / 16));         tempData[idxDown + 1] = Math.min(255, Math.max(0, tempData[idxDown + 1] + errG * 5 / 16));         tempData[idxDown + 2] = Math.min(255, Math.max(0, tempData[idxDown + 2] + errB * 5 / 16));         if (x + 1 < width) {           const idxDownRight = idx + width * 4 + 4;           tempData[idxDownRight] = Math.min(255, Math.max(0, tempData[idxDownRight] + errR * 1 / 16));           tempData[idxDownRight + 1] = Math.min(255, Math.max(0, tempData[idxDownRight + 1] + errG * 1 / 16));           tempData[idxDownRight + 2] = Math.min(255, Math.max(0, tempData[idxDownRight + 2] + errB * 1 / 16));         }       }     }   }    for (let y = 0; y < height; y++) {     for (let x = 0; x < width; x++) {       const idx = (y * width + x) * 4;       const r = tempData[idx];       const g = tempData[idx + 1];       const b = tempData[idx + 2];        const closest = findClosestColor(r, g, b);       data[idx] = closest.r;       data[idx + 1] = closest.g;       data[idx + 2] = closest.b;     }   }    return imageData; }  function atkinsonDither(imageData, strength) {   const width = imageData.width;   const height = imageData.height;   const data = imageData.data;   const tempData = new Uint8ClampedArray(data);    for (let y = 0; y < height; y++) {     for (let x = 0; x < width; x++) {       const idx = (y * width + x) * 4;       const r = tempData[idx];       const g = tempData[idx + 1];       const b = tempData[idx + 2];        const closest = findClosestColor(r, g, b);        data[idx] = closest.r;       data[idx + 1] = closest.g;       data[idx + 2] = closest.b;        const errR = (r - closest.r) * strength;       const errG = (g - closest.g) * strength;       const errB = (b - closest.b) * strength;        const fraction = 1 / 8;        if (x + 1 < width) {         const idxRight = idx + 4;         tempData[idxRight] = Math.min(255, Math.max(0, tempData[idxRight] + errR * fraction));         tempData[idxRight + 1] = Math.min(255, Math.max(0, tempData[idxRight + 1] + errG * fraction));         tempData[idxRight + 2] = Math.min(255, Math.max(0, tempData[idxRight + 2] + errB * fraction));       }       if (x + 2 < width) {         const idxRight2 = idx + 8;         tempData[idxRight2] = Math.min(255, Math.max(0, tempData[idxRight2] + errR * fraction));         tempData[idxRight2 + 1] = Math.min(255, Math.max(0, tempData[idxRight2 + 1] + errG * fraction));         tempData[idxRight2 + 2] = Math.min(255, Math.max(0, tempData[idxRight2 + 2] + errB * fraction));       }       if (y + 1 < height) {         if (x > 0) {           const idxDownLeft = idx + width * 4 - 4;           tempData[idxDownLeft] = Math.min(255, Math.max(0, tempData[idxDownLeft] + errR * fraction));           tempData[idxDownLeft + 1] = Math.min(255, Math.max(0, tempData[idxDownLeft + 1] + errG * fraction));           tempData[idxDownLeft + 2] = Math.min(255, Math.max(0, tempData[idxDownLeft + 2] + errB * fraction));         }         const idxDown = idx + width * 4;         tempData[idxDown] = Math.min(255, Math.max(0, tempData[idxDown] + errR * fraction));         tempData[idxDown + 1] = Math.min(255, Math.max(0, tempData[idxDown + 1] + errG * fraction));         tempData[idxDown + 2] = Math.min(255, Math.max(0, tempData[idxDown + 2] + errB * fraction));         if (x + 1 < width) {           const idxDownRight = idx + width * 4 + 4;           tempData[idxDownRight] = Math.min(255, Math.max(0, tempData[idxDownRight] + errR * fraction));           tempData[idxDownRight + 1] = Math.min(255, Math.max(0, tempData[idxDownRight + 1] + errG * fraction));           tempData[idxDownRight + 2] = Math.min(255, Math.max(0, tempData[idxDownRight + 2] + errB * fraction));         }       }       if (y + 2 < height) {         const idxDown2 = idx + width * 8;         tempData[idxDown2] = Math.min(255, Math.max(0, tempData[idxDown2] + errR * fraction));         tempData[idxDown2 + 1] = Math.min(255, Math.max(0, tempData[idxDown2 + 1] + errG * fraction));         tempData[idxDown2 + 2] = Math.min(255, Math.max(0, tempData[idxDown2 + 2] + errB * fraction));       }     }   }    return imageData; }  function stuckiDither(imageData, strength) {   // 执行Stucki错误扩散算法以处理图像   const width = imageData.width;   const height = imageData.height;   const data = imageData.data;   const tempData = new Uint8ClampedArray(data);    for (let y = 0; y < height; y++) {     for (let x = 0; x < width; x++) {       const idx = (y * width + x) * 4;       const r = tempData[idx];       const g = tempData[idx + 1];       const b = tempData[idx + 2];        const closest = findClosestColor(r, g, b);        const errR = (r - closest.r) * strength;       const errG = (g - closest.g) * strength;       const errB = (b - closest.b) * strength;        const divisor = 42;        if (x + 1 < width) {         const idxRight = idx + 4;         tempData[idxRight] = Math.min(255, Math.max(0, tempData[idxRight] + errR * 8 / divisor));         tempData[idxRight + 1] = Math.min(255, Math.max(0, tempData[idxRight + 1] + errG * 8 / divisor));         tempData[idxRight + 2] = Math.min(255, Math.max(0, tempData[idxRight + 2] + errB * 8 / divisor));       }       if (x + 2 < width) {         const idxRight2 = idx + 8;         tempData[idxRight2] = Math.min(255, Math.max(0, tempData[idxRight2] + errR * 4 / divisor));         tempData[idxRight2 + 1] = Math.min(255, Math.max(0, tempData[idxRight2 + 1] + errG * 4 / divisor));         tempData[idxRight2 + 2] = Math.min(255, Math.max(0, tempData[idxRight2 + 2] + errB * 4 / divisor));       }       if (y + 1 < height) {         if (x > 1) {           const idxDownLeft2 = idx + width * 4 - 8;           tempData[idxDownLeft2] = Math.min(255, Math.max(0, tempData[idxDownLeft2] + errR * 2 / divisor));           tempData[idxDownLeft2 + 1] = Math.min(255, Math.max(0, tempData[idxDownLeft2 + 1] + errG * 2 / divisor));           tempData[idxDownLeft2 + 2] = Math.min(255, Math.max(0, tempData[idxDownLeft2 + 2] + errB * 2 / divisor));         }         if (x > 0) {           const idxDownLeft = idx + width * 4 - 4;           tempData[idxDownLeft] = Math.min(255, Math.max(0, tempData[idxDownLeft] + errR * 4 / divisor));           tempData[idxDownLeft + 1] = Math.min(255, Math.max(0, tempData[idxDownLeft + 1] + errG * 4 / divisor));           tempData[idxDownLeft + 2] = Math.min(255, Math.max(0, tempData[idxDownLeft + 2] + errB * 4 / divisor));         }         const idxDown = idx + width * 4;         tempData[idxDown] = Math.min(255, Math.max(0, tempData[idxDown] + errR * 8 / divisor));         tempData[idxDown + 1] = Math.min(255, Math.max(0, tempData[idxDown + 1] + errG * 8 / divisor));         tempData[idxDown + 2] = Math.min(255, Math.max(0, tempData[idxDown + 2] + errB * 8 / divisor));         if (x + 1 < width) {           const idxDownRight1 = idx + width * 4 + 4;           tempData[idxDownRight1] = Math.min(255, Math.max(0, tempData[idxDownRight1] + errR * 4 / divisor));           tempData[idxDownRight1 + 1] = Math.min(255, Math.max(0, tempData[idxDownRight1 + 1] + errG * 4 / divisor));           tempData[idxDownRight1 + 2] = Math.min(255, Math.max(0, tempData[idxDownRight1 + 2] + errB * 4 / divisor));         }         if (x + 2 < width) {           const idxDownRight2 = idx + width * 4 + 8;           tempData[idxDownRight2] = Math.min(255, Math.max(0, tempData[idxDownRight2] + errR * 2 / divisor));           tempData[idxDownRight2 + 1] = Math.min(255, Math.max(0, tempData[idxDownRight2 + 1] + errG * 2 / divisor));           tempData[idxDownRight2 + 2] = Math.min(255, Math.max(0, tempData[idxDownRight2 + 2] + errB * 2 / divisor));         }       }       if (y + 2 < height) {         if (x > 1) {           const idxDown2Left2 = idx + width * 8 - 8;           tempData[idxDown2Left2] = Math.min(255, Math.max(0, tempData[idxDown2Left2] + errR * 1 / divisor));           tempData[idxDown2Left2 + 1] = Math.min(255, Math.max(0, tempData[idxDown2Left2 + 1] + errG * 1 / divisor));           tempData[idxDown2Left2 + 2] = Math.min(255, Math.max(0, tempData[idxDown2Left2 + 2] + errB * 1 / divisor));         }         if (x > 0) {           const idxDown2Left = idx + width * 8 - 4;           tempData[idxDown2Left] = Math.min(255, Math.max(0, tempData[idxDown2Left] + errR * 2 / divisor));           tempData[idxDown2Left + 1] = Math.min(255, Math.max(0, tempData[idxDown2Left + 1] + errG * 2 / divisor));           tempData[idxDown2Left + 2] = Math.min(255, Math.max(0, tempData[idxDown2Left + 2] + errB * 2 / divisor));         }         const idxDown2 = idx + width * 8;         tempData[idxDown2] = Math.min(255, Math.max(0, tempData[idxDown2] + errR * 4 / divisor));         tempData[idxDown2 + 1] = Math.min(255, Math.max(0, tempData[idxDown2 + 1] + errG * 4 / divisor));         tempData[idxDown2 + 2] = Math.min(255, Math.max(0, tempData[idxDown2 + 2] + errB * 4 / divisor));         if (x + 1 < width) {           const idxDown2Right = idx + width * 8 + 4;           tempData[idxDown2Right] = Math.min(255, Math.max(0, tempData[idxDown2Right] + errR * 2 / divisor));           tempData[idxDown2Right + 1] = Math.min(255, Math.max(0, tempData[idxDown2Right + 1] + errG * 2 / divisor));           tempData[idxDown2Right + 2] = Math.min(255, Math.max(0, tempData[idxDown2Right + 2] + errB * 2 / divisor));         }         if (x + 2 < width) {           const idxDown2Right2 = idx + width * 8 + 8;           tempData[idxDown2Right2] = Math.min(255, Math.max(0, tempData[idxDown2Right2] + errR * 1 / divisor));           tempData[idxDown2Right2 + 1] = Math.min(255, Math.max(0, tempData[idxDown2Right2 + 1] + errG * 1 / divisor));           tempData[idxDown2Right2 + 2] = Math.min(255, Math.max(0, tempData[idxDown2Right2 + 2] + errB * 1 / divisor));         }       }     }   }    for (let y = 0; y < height; y++) {     for (let x = 0; x < width; x++) {       const idx = (y * width + x) * 4;       const r = tempData[idx];       const g = tempData[idx + 1];       const b = tempData[idx + 2];        const closest = findClosestColor(r, g, b);       data[idx] = closest.r;       data[idx + 1] = closest.g;       data[idx + 2] = closest.b;     }   }    return imageData; }  function jarvisDither(imageData, strength) {   const width = imageData.width;   const height = imageData.height;   const data = imageData.data;   const tempData = new Uint8ClampedArray(data);    for (let y = 0; y < height; y++) {     for (let x = 0; x < width; x++) {       const idx = (y * width + x) * 4;       const r = tempData[idx];       const g = tempData[idx + 1];       const b = tempData[idx + 2];        const closest = findClosestColor(r, g, b);        data[idx] = closest.r;       data[idx + 1] = closest.g;       data[idx + 2] = closest.b;        const errR = (r - closest.r) * strength;       const errG = (g - closest.g) * strength;       const errB = (b - closest.b) * strength;        const divisor = 48;        if (x + 1 < width) {         const idxRight = idx + 4;         tempData[idxRight] = Math.min(255, Math.max(0, tempData[idxRight] + errR * 7 / divisor));         tempData[idxRight + 1] = Math.min(255, Math.max(0, tempData[idxRight + 1] + errG * 7 / divisor));         tempData[idxRight + 2] = Math.min(255, Math.max(0, tempData[idxRight + 2] + errB * 7 / divisor));       }       if (x + 2 < width) {         const idxRight2 = idx + 8;         tempData[idxRight2] = Math.min(255, Math.max(0, tempData[idxRight2] + errR * 5 / divisor));         tempData[idxRight2 + 1] = Math.min(255, Math.max(0, tempData[idxRight2 + 1] + errG * 5 / divisor));         tempData[idxRight2 + 2] = Math.min(255, Math.max(0, tempData[idxRight2 + 2] + errB * 5 / divisor));       }       if (y + 1 < height) {         if (x > 1) {           const idxDownLeft2 = idx + width * 4 - 8;           tempData[idxDownLeft2] = Math.min(255, Math.max(0, tempData[idxDownLeft2] + errR * 3 / divisor));           tempData[idxDownLeft2 + 1] = Math.min(255, Math.max(0, tempData[idxDownLeft2 + 1] + errG * 3 / divisor));           tempData[idxDownLeft2 + 2] = Math.min(255, Math.max(0, tempData[idxDownLeft2 + 2] + errB * 3 / divisor));         }         if (x > 0) {           const idxDownLeft = idx + width * 4 - 4;           tempData[idxDownLeft] = Math.min(255, Math.max(0, tempData[idxDownLeft] + errR * 5 / divisor));           tempData[idxDownLeft + 1] = Math.min(255, Math.max(0, tempData[idxDownLeft + 1] + errG * 5 / divisor));           tempData[idxDownLeft + 2] = Math.min(255, Math.max(0, tempData[idxDownLeft + 2] + errB * 5 / divisor));         }         const idxDown = idx + width * 4;         tempData[idxDown] = Math.min(255, Math.max(0, tempData[idxDown] + errR * 7 / divisor));         tempData[idxDown + 1] = Math.min(255, Math.max(0, tempData[idxDown + 1] + errG * 7 / divisor));         tempData[idxDown + 2] = Math.min(255, Math.max(0, tempData[idxDown + 2] + errB * 7 / divisor));         if (x + 1 < width) {           const idxDownRight = idx + width * 4 + 4;           tempData[idxDownRight] = Math.min(255, Math.max(0, tempData[idxDownRight] + errR * 5 / divisor));           tempData[idxDownRight + 1] = Math.min(255, Math.max(0, tempData[idxDownRight + 1] + errG * 5 / divisor));           tempData[idxDownRight + 2] = Math.min(255, Math.max(0, tempData[idxDownRight + 2] + errB * 5 / divisor));         }         if (x + 2 < width) {           const idxDownRight2 = idx + width * 4 + 8;           tempData[idxDownRight2] = Math.min(255, Math.max(0, tempData[idxDownRight2] + errR * 3 / divisor));           tempData[idxDownRight2 + 1] = Math.min(255, Math.max(0, tempData[idxDownRight2 + 1] + errG * 3 / divisor));           tempData[idxDownRight2 + 2] = Math.min(255, Math.max(0, tempData[idxDownRight2 + 2] + errB * 3 / divisor));         }       }       if (y + 2 < height) {         if (x > 1) {           const idxDown2Left2 = idx + width * 8 - 8;           tempData[idxDown2Left2] = Math.min(255, Math.max(0, tempData[idxDown2Left2] + errR * 1 / divisor));           tempData[idxDown2Left2 + 1] = Math.min(255, Math.max(0, tempData[idxDown2Left2 + 1] + errG * 1 / divisor));           tempData[idxDown2Left2 + 2] = Math.min(255, Math.max(0, tempData[idxDown2Left2 + 2] + errB * 1 / divisor));         }         if (x > 0) {           const idxDown2Left = idx + width * 8 - 4;           tempData[idxDown2Left] = Math.min(255, Math.max(0, tempData[idxDown2Left] + errR * 3 / divisor));           tempData[idxDown2Left + 1] = Math.min(255, Math.max(0, tempData[idxDown2Left + 1] + errG * 3 / divisor));           tempData[idxDown2Left + 2] = Math.min(255, Math.max(0, tempData[idxDown2Left + 2] + errB * 3 / divisor));         }         const idxDown2 = idx + width * 8;         tempData[idxDown2] = Math.min(255, Math.max(0, tempData[idxDown2] + errR * 5 / divisor));         tempData[idxDown2 + 1] = Math.min(255, Math.max(0, tempData[idxDown2 + 1] + errG * 5 / divisor));         tempData[idxDown2 + 2] = Math.min(255, Math.max(0, tempData[idxDown2 + 2] + errB * 5 / divisor));         if (x + 1 < width) {           const idxDown2Right = idx + width * 8 + 4;           tempData[idxDown2Right] = Math.min(255, Math.max(0, tempData[idxDown2Right] + errR * 3 / divisor));           tempData[idxDown2Right + 1] = Math.min(255, Math.max(0, tempData[idxDown2Right + 1] + errG * 3 / divisor));           tempData[idxDown2Right + 2] = Math.min(255, Math.max(0, tempData[idxDown2Right + 2] + errB * 3 / divisor));         }         if (x + 2 < width) {           const idxDown2Right2 = idx + width * 8 + 8;           tempData[idxDown2Right2] = Math.min(255, Math.max(0, tempData[idxDown2Right2] + errR * 1 / divisor));           tempData[idxDown2Right2 + 1] = Math.min(255, Math.max(0, tempData[idxDown2Right2 + 1] + errG * 1 / divisor));           tempData[idxDown2Right2 + 2] = Math.min(255, Math.max(0, tempData[idxDown2Right2 + 2] + errB * 1 / divisor));         }       }     }   }    return imageData; }  function bayerDither(imageData, strength) {   const width = imageData.width;   const height = imageData.height;   const data = imageData.data;    // 8x8 Bayer matrix (normalized to 0-1 range)   const bayerMatrix = [     [0, 32, 8, 40, 2, 34, 10, 42],     [48, 16, 56, 24, 50, 18, 58, 26],     [12, 44, 4, 36, 14, 46, 6, 38],     [60, 28, 52, 20, 62, 30, 54, 22],     [3, 35, 11, 43, 1, 33, 9, 41],     [51, 19, 59, 27, 49, 17, 57, 25],     [15, 47, 7, 39, 13, 45, 5, 37],     [63, 31, 55, 23, 61, 29, 53, 21]   ];    const matrixSize = 8;   const maxThreshold = 64;    for (let y = 0; y < height; y++) {     for (let x = 0; x < width; x++) {       const idx = (y * width + x) * 4;       const r = data[idx];       const g = data[idx + 1];       const b = data[idx + 2];        // Get threshold from Bayer matrix       const matrixX = x % matrixSize;       const matrixY = y % matrixSize;       const threshold = (bayerMatrix[matrixY][matrixX] / maxThreshold) * 255;        // Apply dithering with strength factor       const adjustedR = r + (threshold - 127.5) * strength;       const adjustedG = g + (threshold - 127.5) * strength;       const adjustedB = b + (threshold - 127.5) * strength;        // Clamp values       const clampedR = Math.min(255, Math.max(0, adjustedR));       const clampedG = Math.min(255, Math.max(0, adjustedG));       const clampedB = Math.min(255, Math.max(0, adjustedB));        // Find closest color in palette       const closest = findClosestColor(clampedR, clampedG, clampedB);              data[idx] = closest.r;       data[idx + 1] = closest.g;       data[idx + 2] = closest.b;     }   }    return imageData; }  function ditherImage(imageData) {   const ditherType = document.getElementById('ditherType').value;   const ditherStrength = parseFloat(document.getElementById('ditherStrength').value);    switch (ditherType) {     case 'floydSteinberg':       return floydSteinbergDither(imageData, ditherStrength);     case 'atkinson':       return atkinsonDither(imageData, ditherStrength);     case 'stucki':       return stuckiDither(imageData, ditherStrength);     case 'jarvis':       return jarvisDither(imageData, ditherStrength);     case 'bayer':       return bayerDither(imageData, ditherStrength);     default:       return imageData;   } }  function decodeProcessedData(processedData, width, height, mode) {   const imageData = new ImageData(width, height);   const data = imageData.data;    if (mode === 'sixColor') {     for (let y = 0; y < height; y++) {       for (let x = 0; x < width; x++) {         const newIndex = (x * height) + (height - 1 - y);         const value = processedData[newIndex];         const color = rgbPalette.find(c => c.value === value) || rgbPalette[5]; // 默认白色         const index = (y * width + x) * 4;         data[index] = color.r;         data[index + 1] = color.g;         data[index + 2] = color.b;         data[index + 3] = 255; // Alpha 透明度       }     }   } else if (mode === 'fourColor') {     const fourColorValues = [       { value: 0x00, r: 0, g: 0, b: 0 },      // 黑色       { value: 0x01, r: 255, g: 255, b: 255 }, // 白色       { value: 0x03, r: 255, g: 0, b: 0 },     // 红色       { value: 0x02, r: 255, g: 255, b: 0 }    // 黄色     ];     for (let y = 0; y < height; y++) {       for (let x = 0; x < width; x++) {         const newIndex = (y * width + x) / 4 | 0;         const shift = 6 - ((x % 4) * 2);         const value = (processedData[newIndex] >> shift) & 0x03;         const color = fourColorValues.find(c => c.value === value) || fourColorValues[1]; // 默认白色         const index = (y * width + x) * 4;         data[index] = color.r;         data[index + 1] = color.g;         data[index + 2] = color.b;         data[index + 3] = 255;       }     }   } else if (mode === 'blackWhiteColor') {     const byteWidth = Math.ceil(width / 8);     for (let y = 0; y < height; y++) {       for (let x = 0; x < width; x++) {         const byteIndex = y * byteWidth + Math.floor(x / 8);         const bitIndex = 7 - (x % 8);         const bit = (processedData[byteIndex] >> bitIndex) & 1;         const index = (y * width + x) * 4;         data[index] = bit ? 255 : 0; // 白或黑         data[index + 1] = bit ? 255 : 0;         data[index + 2] = bit ? 255 : 0;         data[index + 3] = 255;       }     }   } else if (mode === 'threeColor') {     const byteWidth = Math.ceil(width / 8);     const blackWhiteData = processedData.slice(0, byteWidth * height);     const redWhiteData = processedData.slice(byteWidth * height);     for (let y = 0; y < height; y++) {       for (let x = 0; x < width; x++) {         const byteIndex = y * byteWidth + Math.floor(x / 8);         const bitIndex = 7 - (x % 8);         const blackWhiteBit = (blackWhiteData[byteIndex] >> bitIndex) & 1;         const redWhiteBit = (redWhiteData[byteIndex] >> bitIndex) & 1;         const index = (y * width + x) * 4;         if (!redWhiteBit) {           // 红色           data[index] = 255;           data[index + 1] = 0;           data[index + 2] = 0;         } else {           // 黑或白           data[index] = blackWhiteBit ? 255 : 0;           data[index + 1] = blackWhiteBit ? 255 : 0;           data[index + 2] = blackWhiteBit ? 255 : 0;         }         data[index + 3] = 255;       }     }   }    return imageData; }  function processImageData(imageData) {   const width = imageData.width;   const height = imageData.height;   const data = imageData.data;   const mode = document.getElementById('ditherMode').value;    let processedData;    if (mode === 'sixColor') {     processedData = new Uint8Array(width * height);     for (let y = 0; y < height; y++) {       for (let x = 0; x < width; x++) {         const index = (y * width + x) * 4;         const r = data[index];         const g = data[index + 1];         const b = data[index + 2];          const closest = findClosestColor(r, g, b);         const newIndex = (x * height) + (height - 1 - y);         processedData[newIndex] = closest.value;       }     }   } else if (mode === 'fourColor') {     processedData = new Uint8Array(Math.ceil((width * height) / 4));     for (let y = 0; y < height; y++) {       for (let x = 0; x < width; x++) {         const index = (y * width + x) * 4;         const r = data[index];         const g = data[index + 1];         const b = data[index + 2];         const closest = findClosestColor(r, g, b); // 使用 fourColorPalette         const colorValue = closest.value; // 0x00 (黑), 0x01 (白), 0x02 (红), 0x03 (黄)         const newIndex = (y * width + x) / 4 | 0;         const shift = 6 - ((x % 4) * 2);         processedData[newIndex] |= (colorValue << shift);       }     }   } else if (mode === 'blackWhiteColor') {     const byteWidth = Math.ceil(width / 8);     processedData = new Uint8Array(byteWidth * height);     const threshold = 140;      for (let y = 0; y < height; y++) {       for (let x = 0; x < width; x++) {         const index = (y * width + x) * 4;         const r = data[index];         const g = data[index + 1];         const b = data[index + 2];         const grayscale = Math.round(0.299 * r + 0.587 * g + 0.114 * b);         const bit = grayscale >= threshold ? 1 : 0;         const byteIndex = y * byteWidth + Math.floor(x / 8);         const bitIndex = 7 - (x % 8);         processedData[byteIndex] |= (bit << bitIndex);       }     }   } else if (mode === 'threeColor') {     const byteWidth = Math.ceil(width / 8);     const blackWhiteThreshold = 140;     const redThreshold = 160;      const blackWhiteData = new Uint8Array(height * byteWidth);     const redWhiteData = new Uint8Array(height * byteWidth);      for (let y = 0; y < height; y++) {       for (let x = 0; x < width; x++) {         const index = (y * width + x) * 4;         const r = data[index];         const g = data[index + 1];         const b = data[index + 2];         const grayscale = Math.round(0.299 * r + 0.587 * g + 0.114 * b);          const blackWhiteBit = grayscale >= blackWhiteThreshold ? 1 : 0;         const blackWhiteByteIndex = y * byteWidth + Math.floor(x / 8);         const blackWhiteBitIndex = 7 - (x % 8);         if (blackWhiteBit) {           blackWhiteData[blackWhiteByteIndex] |= (0x01 << blackWhiteBitIndex);         } else {           blackWhiteData[blackWhiteByteIndex] &= ~(0x01 << blackWhiteBitIndex);         }          const redWhiteBit = (r > redThreshold && r > g && r > b) ? 0 : 1;         const redWhiteByteIndex = y * byteWidth + Math.floor(x / 8);         const redWhiteBitIndex = 7 - (x % 8);         if (redWhiteBit) {           redWhiteData[redWhiteByteIndex] |= (0x01 << redWhiteBitIndex);         } else {           redWhiteData[redWhiteByteIndex] &= ~(0x01 << redWhiteBitIndex);         }       }     }      processedData = new Uint8Array(blackWhiteData.length + redWhiteData.length);     processedData.set(blackWhiteData, 0);     processedData.set(redWhiteData, blackWhiteData.length);   }    return processedData; } 

main.js

let bleDevice, gattServer; let epdService, epdCharacteristic; let startTime, msgIndex, appVersion; let canvas, ctx, textDecoder;  const EpdCmd = {   SET_PINS:  0x00,   INIT:      0x01,   CLEAR:     0x02,   SEND_CMD:  0x03,   SEND_DATA: 0x04,   REFRESH:   0x05,   SLEEP:     0x06,    SET_TIME:  0x20,    WRITE_IMG: 0x30, // v1.6    SET_CONFIG: 0x90,   SYS_RESET:  0x91,   SYS_SLEEP:  0x92,   CFG_ERASE:  0x99, };  const canvasSizes = [   { name: '1.54_152_152', width: 152, height: 152 },   { name: '1.54_200_200', width: 200, height: 200 },   { name: '2.13_212_104', width: 212, height: 104 },   { name: '2.13_250_122', width: 250, height: 122 },   { name: '2.66_296_152', width: 296, height: 152 },   { name: '2.9_296_128', width: 296, height: 128 },   { name: '2.9_384_168', width: 384, height: 168 },   { name: '3.5_384_184', width: 384, height: 184 },   { name: '3.7_416_240', width: 416, height: 240 },   { name: '3.97_800_480', width: 800, height: 480 },   { name: '4.2_400_300', width: 400, height: 300 },   { name: '5.79_792_272', width: 792, height: 272 },   { name: '7.5_800_480', width: 800, height: 480 },   { name: '10.2_960_640', width: 960, height: 640 },   { name: '10.85_1360_480', width: 1360, height: 480 },   { name: '11.6_960_640', width: 960, height: 640 },   { name: '4E_600_400', width: 600, height: 400 },   { name: '7.3E6', width: 480, height: 800 } ];  function hex2bytes(hex) {   for (var bytes = [], c = 0; c < hex.length; c += 2)     bytes.push(parseInt(hex.substr(c, 2), 16));   return new Uint8Array(bytes); }  function bytes2hex(data) {   return new Uint8Array(data).reduce(     function (memo, i) {       return memo + ("0" + i.toString(16)).slice(-2);     }, ""); }  function intToHex(intIn) {   let stringOut = ("0000" + intIn.toString(16)).substr(-4)   return stringOut.substring(2, 4) + stringOut.substring(0, 2); }  function resetVariables() {   gattServer = null;   epdService = null;   epdCharacteristic = null;   msgIndex = 0;   document.getElementById("log").value = ''; }  async function write(cmd, data, withResponse = true) {   if (!epdCharacteristic) {     addLog("服务不可用,请检查蓝牙连接");     return false;   }   let payload = [cmd];   if (data) {     if (typeof data == 'string') data = hex2bytes(data);     if (data instanceof Uint8Array) data = Array.from(data);     payload.push(...data)   }   addLog(bytes2hex(payload), '⇑');   try {     if (withResponse)       await epdCharacteristic.writeValueWithResponse(Uint8Array.from(payload));     else       await epdCharacteristic.writeValueWithoutResponse(Uint8Array.from(payload));   } catch (e) {     console.error(e);     if (e.message) addLog("write: " + e.message);     return false;   }   return true; }  async function writeImage(data, step = 'bw') {   const chunkSize = document.getElementById('mtusize').value - 2;   const interleavedCount = document.getElementById('interleavedcount').value;   const count = Math.round(data.length / chunkSize);   let chunkIdx = 0;   let noReplyCount = interleavedCount;    for (let i = 0; i < data.length; i += chunkSize) {     let currentTime = (new Date().getTime() - startTime) / 1000.0;     setStatus(`${step == 'bw' ? '黑白' : '颜色'}块: ${chunkIdx + 1}/${count + 1}, 总用时: ${currentTime}s`);     const payload = [       (step == 'bw' ? 0x0F : 0x00) | (i == 0 ? 0x00 : 0xF0),       ...data.slice(i, i + chunkSize),     ];     if (noReplyCount > 0) {       await write(EpdCmd.WRITE_IMG, payload, false);       noReplyCount--;     } else {       await write(EpdCmd.WRITE_IMG, payload, true);       noReplyCount = interleavedCount;     }     chunkIdx++;   } }  async function setDriver() {   await write(EpdCmd.SET_PINS, document.getElementById("epdpins").value);   await write(EpdCmd.INIT, document.getElementById("epddriver").value); }  async function syncTime(mode) {   const timestamp = new Date().getTime() / 1000;   const data = new Uint8Array([     (timestamp >> 24) & 0xFF,     (timestamp >> 16) & 0xFF,     (timestamp >> 8) & 0xFF,     timestamp & 0xFF,     -(new Date().getTimezoneOffset() / 60),     mode   ]);   if (await write(EpdCmd.SET_TIME, data)) {     addLog("时间已同步!");     addLog("屏幕刷新完成前请不要操作。");   } }  async function clearScreen() {   if (confirm('确认清除屏幕内容?')) {     await write(EpdCmd.CLEAR);     addLog("清屏指令已发送!");     addLog("屏幕刷新完成前请不要操作。");   } }  async function sendcmd() {   const cmdTXT = document.getElementById('cmdTXT').value;   if (cmdTXT == '') return;   const bytes = hex2bytes(cmdTXT);   await write(bytes[0], bytes.length > 1 ? bytes.slice(1) : null); }  async function sendimg() {   const canvasSize = document.getElementById('canvasSize').value;   const ditherMode = document.getElementById('ditherMode').value;   const epdDriverSelect = document.getElementById('epddriver');   const selectedOption = epdDriverSelect.options[epdDriverSelect.selectedIndex];    if (selectedOption.getAttribute('data-size') !== canvasSize) {     addLog(`画布尺寸和驱动不匹配,请重新选择。`);     return;   }   if (selectedOption.getAttribute('data-color') !== ditherMode) {     addLog(`颜色模式和驱动不匹配,请重新选择。`);     return;   }    startTime = new Date().getTime();   const status = document.getElementById("status");   status.parentElement.style.display = "block";    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);   const processedData = processImageData(imageData);    updateButtonStatus(true);    if (ditherMode === 'fourColor') {     await writeImage(processedData, 'color');   } else if (ditherMode === 'threeColor') {     const halfLength = Math.floor(processedData.length / 2);     await writeImage(processedData.slice(0, halfLength), 'bw');     await writeImage(processedData.slice(halfLength), 'red');   } else if (ditherMode === 'blackWhiteColor') {     await writeImage(processedData, 'bw');   } else {     addLog("当前固件不支持此颜色模式。");     updateButtonStatus();     return;   }    await write(EpdCmd.REFRESH);   updateButtonStatus();    const sendTime = (new Date().getTime() - startTime) / 1000.0;   addLog(`发送完成!耗时: ${sendTime}s`);   setStatus(`发送完成!耗时: ${sendTime}s`);   addLog("屏幕刷新完成前请不要操作。");   setTimeout(() => {     status.parentElement.style.display = "none";   }, 5000); }  function downloadDataArray() {   const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);   const processedData = processImageData(imageData);   const mode = document.getElementById('ditherMode').value;    if (mode === 'sixColor' && processedData.length !== canvas.width * canvas.height) {     console.log(`错误:预期${canvas.width * canvas.height}字节,但得到${processedData.length}字节`);     addLog('数组大小不匹配。请检查图像尺寸和模式。');     return;   }    const dataLines = [];   for (let i = 0; i < processedData.length; i++) {     const hexValue = (processedData[i] & 0xff).toString(16).padStart(2, '0');     dataLines.push(`0x${hexValue}`);   }    const formattedData = [];   for (let i = 0; i < dataLines.length; i += 16) {     formattedData.push(dataLines.slice(i, i + 16).join(', '));   }    const colorModeValue = mode === 'sixColor' ? 0 : mode === 'fourColor' ? 1 : mode === 'blackWhiteColor' ? 2 : 3;   const arrayContent = [     'const uint8_t imageData[] PROGMEM = {',     formattedData.join(',n'),     '};',     `const uint16_t imageWidth = ${canvas.width};`,     `const uint16_t imageHeight = ${canvas.height};`,     `const uint8_t colorMode = ${colorModeValue};`   ].join('n');    const blob = new Blob([arrayContent], { type: 'text/plain' });   const link = document.createElement('a');   link.download = 'imagedata.h';   link.href = URL.createObjectURL(blob);   link.click();   URL.revokeObjectURL(link.href); }  function updateButtonStatus(forceDisabled = false) {   const connected = gattServer != null && gattServer.connected;   const status = forceDisabled ? 'disabled' : (connected ? null : 'disabled');   document.getElementById("reconnectbutton").disabled = (gattServer == null || gattServer.connected) ? 'disabled' : null;   document.getElementById("sendcmdbutton").disabled = status;   document.getElementById("calendarmodebutton").disabled = status;   document.getElementById("clockmodebutton").disabled = status;   document.getElementById("clearscreenbutton").disabled = status;   document.getElementById("sendimgbutton").disabled = status;   document.getElementById("setDriverbutton").disabled = status; }  function disconnect() {   updateButtonStatus();   resetVariables();   addLog('已断开连接.');   document.getElementById("connectbutton").innerHTML = '连接'; }  async function preConnect() {   if (gattServer != null && gattServer.connected) {     if (bleDevice != null && bleDevice.gatt.connected) {       bleDevice.gatt.disconnect();     }   }   else {     resetVariables();     try {       bleDevice = await navigator.bluetooth.requestDevice({         optionalServices: ['62750001-d828-918d-fb46-b6c11c675aec'],         acceptAllDevices: true       });     } catch (e) {       console.error(e);       if (e.message) addLog("requestDevice: " + e.message);       addLog("请检查蓝牙是否已开启,且使用的浏览器支持蓝牙!建议使用以下浏览器:");       addLog("• 电脑: Chrome/Edge");       addLog("• Android: Chrome/Edge");       addLog("• iOS: Bluefy 浏览器");       return;     }      await bleDevice.addEventListener('gattserverdisconnected', disconnect);     setTimeout(async function () { await connect(); }, 300);   } }  async function reConnect() {   if (bleDevice != null && bleDevice.gatt.connected)     bleDevice.gatt.disconnect();   resetVariables();   addLog("正在重连");   setTimeout(async function () { await connect(); }, 300); }  function handleNotify(value, idx) {   const data = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);   if (idx == 0) {     addLog(`收到配置:${bytes2hex(data)}`);     const epdpins = document.getElementById("epdpins");     const epddriver = document.getElementById("epddriver");     epdpins.value = bytes2hex(data.slice(0, 7));     if (data.length > 10) epdpins.value += bytes2hex(data.slice(10, 11));     epddriver.value = bytes2hex(data.slice(7, 8));     updateDitcherOptions();   } else {     if (textDecoder == null) textDecoder = new TextDecoder();     addLog(textDecoder.decode(data), '⇓');   } }  async function connect() {   if (bleDevice == null || epdCharacteristic != null) return;    try {     addLog("正在连接: " + bleDevice.name);     gattServer = await bleDevice.gatt.connect();     addLog('  找到 GATT Server');     epdService = await gattServer.getPrimaryService('62750001-d828-918d-fb46-b6c11c675aec');     addLog('  找到 EPD Service');     epdCharacteristic = await epdService.getCharacteristic('62750002-d828-918d-fb46-b6c11c675aec');     addLog('  找到 Characteristic');   } catch (e) {     console.error(e);     if (e.message) addLog("connect: " + e.message);     disconnect();     return;   }    try {     const versionCharacteristic = await epdService.getCharacteristic('62750003-d828-918d-fb46-b6c11c675aec');     const versionData = await versionCharacteristic.readValue();     appVersion = versionData.getUint8(0);     addLog(`固件版本: 0x${appVersion.toString(16)}`);   } catch (e) {     console.error(e);     appVersion = 0x15;   }    if (appVersion < 0x16) {     const oldURL = "https://tsl0922.github.io/EPD-nRF5/v1.5";     alert("!!!注意!!!n当前固件版本过低,可能无法正常使用部分功能,建议升级到最新版本。");     if (confirm('是否访问旧版本上位机?')) location.href = oldURL;     setTimeout(() => {       addLog(`如遇到问题,可访问旧版本上位机: ${oldURL}`);     }, 500);   }    try {     await epdCharacteristic.startNotifications();     epdCharacteristic.addEventListener('characteristicvaluechanged', (event) => {       handleNotify(event.target.value, msgIndex++);     });   } catch (e) {     console.error(e);     if (e.message) addLog("startNotifications: " + e.message);   }    await write(EpdCmd.INIT);    document.getElementById("connectbutton").innerHTML = '断开';   updateButtonStatus(); }  function setStatus(statusText) {   document.getElementById("status").innerHTML = statusText; }  function addLog(logTXT, action = '') {   const log = document.getElementById("log");   const now = new Date();   const time = String(now.getHours()).padStart(2, '0') + ":" +     String(now.getMinutes()).padStart(2, '0') + ":" +     String(now.getSeconds()).padStart(2, '0') + " ";    const logEntry = document.createElement('div');   const timeSpan = document.createElement('span');   timeSpan.className = 'time';   timeSpan.textContent = time;   logEntry.appendChild(timeSpan);    if (action !== '') {     const actionSpan = document.createElement('span');     actionSpan.className = 'action';     actionSpan.innerHTML = action;     logEntry.appendChild(actionSpan);   }   logEntry.appendChild(document.createTextNode(logTXT));    log.appendChild(logEntry);   log.scrollTop = log.scrollHeight;    while (log.childNodes.length > 20) {     log.removeChild(log.firstChild);   } }  function clearLog() {   document.getElementById("log").innerHTML = ''; }  function updateCanvasSize() {   const selectedSizeName = document.getElementById('canvasSize').value;   const selectedSize = canvasSizes.find(size => size.name === selectedSizeName);    canvas.width = selectedSize.width;   canvas.height = selectedSize.height;    updateImage(false); }  function updateDitcherOptions() {   const epdDriverSelect = document.getElementById('epddriver');   const selectedOption = epdDriverSelect.options[epdDriverSelect.selectedIndex];   const colorMode = selectedOption.getAttribute('data-color');   const canvasSize = selectedOption.getAttribute('data-size');    if (colorMode) document.getElementById('ditherMode').value = colorMode;   if (canvasSize) document.getElementById('canvasSize').value = canvasSize;    updateCanvasSize(); // always update image }  function updateImage(clear = false) {   const image_file = document.getElementById('image_file');   if (image_file.files.length == 0) return;    if (clear) clearCanvas();    const file = image_file.files[0];   let image = new Image();;   image.src = URL.createObjectURL(file);   image.onload = function (event) {     URL.revokeObjectURL(this.src);     ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);      // Redraw text and lines     redrawTextElements();     redrawLineSegments();      convertDithering()   } }  function clearCanvas() {   if (confirm('清除画布已有内容?')) {     ctx.fillStyle = 'white';     ctx.fillRect(0, 0, canvas.width, canvas.height);     textElements = []; // Clear stored text positions     lineSegments = []; // Clear stored line segments     return true;   }   return false; }  function convertDithering() {   const contrast = parseFloat(document.getElementById('contrast').value);   const currentImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);   const imageData = new ImageData(     new Uint8ClampedArray(currentImageData.data),     currentImageData.width,     currentImageData.height   );    adjustContrast(imageData, contrast);    const mode = document.getElementById('ditherMode').value;   const processedData = processImageData(ditherImage(imageData));   const finalImageData = decodeProcessedData(processedData, canvas.width, canvas.height, mode);   ctx.putImageData(finalImageData, 0, 0); }  function checkDebugMode() {   const link = document.getElementById('debug-toggle');   const urlParams = new URLSearchParams(window.location.search);   const debugMode = urlParams.get('debug');    if (debugMode === 'true') {     document.body.classList.add('debug-mode');     link.innerHTML = '正常模式';     link.setAttribute('href', window.location.pathname);     addLog("注意:开发模式功能已开启!不懂请不要随意修改,否则后果自负!");   } else {     document.body.classList.remove('debug-mode');     link.innerHTML = '开发模式';     link.setAttribute('href', window.location.pathname + '?debug=true');   } }  document.body.onload = () => {   textDecoder = null;   canvas = document.getElementById('canvas');   ctx = canvas.getContext("2d");    ctx.fillStyle = 'white';   ctx.fillRect(0, 0, canvas.width, canvas.height);    initPaintTools();   updateButtonStatus();   checkDebugMode(); }

paint.js

let painting = false; let lastX = 0; let lastY = 0; let brushColor = "#000000"; let brushSize = 2; let currentTool = null; // Start with no tool selected let textElements = []; // Store text elements for re-rendering after dithering let lineSegments = []; // Store line segments for re-rendering after dithering let isTextPlacementMode = false; let draggingCanvasContext = null; // Backup of the canvas for dragging let selectedTextElement = null; // Track the currently selected text for dragging let isDraggingText = false; // Track if we're currently dragging text let dragOffsetX = 0; // Offset from mouse to text position when dragging let dragOffsetY = 0; let textBold = false; // Track if text should be bold let textItalic = false; // Track if text should be italic  function setCanvasTitle(title) {   const canvasTitle = document.querySelector('.canvas-title');   if (canvasTitle) {     canvasTitle.innerText = title;     canvasTitle.style.display = title && title !== '' ? 'block' : 'none';   } }  function initPaintTools() {   document.getElementById('brush-mode').addEventListener('click', () => {     if (currentTool === 'brush') {       setActiveTool(null, '');     } else {       setActiveTool('brush', '画笔模式');       brushColor = document.getElementById('brush-color').value;     }   });      document.getElementById('eraser-mode').addEventListener('click', () => {     if (currentTool === 'eraser') {       setActiveTool(null, '');     } else {       setActiveTool('eraser', '橡皮擦');       brushColor = "#FFFFFF";     }   });    document.getElementById('text-mode').addEventListener('click', () => {     if (currentTool === 'text') {       setActiveTool(null, '');     } else {       setActiveTool('text', '插入文字');       brushColor = document.getElementById('brush-color').value;     }   });      document.getElementById('brush-color').addEventListener('change', (e) => {     brushColor = e.target.value;   });      document.getElementById('brush-size').addEventListener('change', (e) => {     brushSize = parseInt(e.target.value);   });    document.getElementById('add-text-btn').addEventListener('click', startTextPlacement);    // Add event listeners for bold and italic buttons   document.getElementById('text-bold').addEventListener('click', () => {     textBold = !textBold;     document.getElementById('text-bold').classList.toggle('primary', textBold);   });      document.getElementById('text-italic').addEventListener('click', () => {     textItalic = !textItalic;     document.getElementById('text-italic').classList.toggle('primary', textItalic);   });      setupCanvasForPainting();    // Ensure no tool is selected by default   updateToolUI(); }  function setActiveTool(tool, title) {   currentTool = tool;   updateToolUI();    setCanvasTitle(title);    // Cancel any pending text placement   cancelTextPlacement(); }  function updateToolUI() {   // Update UI to reflect active tool or no tool   document.getElementById('brush-mode').classList.toggle('active', currentTool === 'brush');   document.getElementById('eraser-mode').classList.toggle('active', currentTool === 'eraser');   document.getElementById('text-mode').classList.toggle('active', currentTool === 'text');    // Show/hide brush tools   document.querySelectorAll('.brush-tools').forEach(el => {     el.style.display = ['brush', 'text'].includes(currentTool) ? 'flex' : 'none';   });    // Show/hide text tools   document.querySelectorAll('.text-tools').forEach(el => {     el.style.display = currentTool === 'text' ? 'flex' : 'none';   }); }  function setupCanvasForPainting() {   canvas.addEventListener('mousedown', startPaint);   canvas.addEventListener('mousemove', paint);   canvas.addEventListener('mouseup', endPaint);   canvas.addEventListener('mouseleave', endPaint);   canvas.addEventListener('click', handleCanvasClick);      // Touch support   canvas.addEventListener('touchstart', handleTouchStart);   canvas.addEventListener('touchmove', handleTouchMove);   canvas.addEventListener('touchend', endPaint); }  function startPaint(e) {   if (!currentTool) return;    if (currentTool === 'text') {     // Check if we're clicking on a text element to drag     const textElement = findTextElementAt(e);     if (textElement && textElement === selectedTextElement) {       isDraggingText = true;              const rect = canvas.getBoundingClientRect();       const scaleX = canvas.width / rect.width;       const scaleY = canvas.height / rect.height;       const x = (e.clientX - rect.left) * scaleX;       const y = (e.clientY - rect.top) * scaleY;              // Calculate offset for smooth dragging       dragOffsetX = textElement.x - x;       dragOffsetY = textElement.y - y;              return; // Don't start drawing     }   } else {     painting = true;     draw(e);   } }  function endPaint() {   painting = false;   isDraggingText = false;   lastX = 0;   lastY = 0; }  function paint(e) {   if (!currentTool) return;    if (currentTool === 'text') {     if (isDraggingText && selectedTextElement) {       dragText(e);     }   } else {     if (painting) {       draw(e);     }   } }  function draw(e) {   const rect = canvas.getBoundingClientRect();   const scaleX = canvas.width / rect.width;   const scaleY = canvas.height / rect.height;   const x = (e.clientX - rect.left) * scaleX;   const y = (e.clientY - rect.top) * scaleY;      ctx.lineJoin = 'round';   ctx.lineCap = 'round';   ctx.strokeStyle = brushColor;   ctx.lineWidth = brushSize;      ctx.beginPath();      if (lastX === 0 && lastY === 0) {     // For the first point, just do a dot     ctx.moveTo(x, y);     ctx.lineTo(x+0.1, y+0.1);          // Store the dot for redrawing     lineSegments.push({       type: 'dot',       x: x,       y: y,       color: brushColor,       size: brushSize     });   } else {     // Connect to the previous point     ctx.moveTo(lastX, lastY);     ctx.lineTo(x, y);          // Store the line segment for redrawing     lineSegments.push({       type: 'line',       x1: lastX,       y1: lastY,       x2: x,       y2: y,       color: brushColor,       size: brushSize     });   }      ctx.stroke();      lastX = x;   lastY = y; }  function handleCanvasClick(e) {   if (currentTool === 'text' && isTextPlacementMode) {     placeText(e);   } }  // Improve touch handling for text placement function handleTouchStart(e) {     e.preventDefault();     const touch = e.touches[0];          // If in text placement mode, handle as a click     if (currentTool === 'text' && isTextPlacementMode) {         const mouseEvent = new MouseEvent('click', {             clientX: touch.clientX,             clientY: touch.clientY         });         canvas.dispatchEvent(mouseEvent);         return;     }          // Otherwise handle as normal drawing     const mouseEvent = new MouseEvent('mousedown', {         clientX: touch.clientX,         clientY: touch.clientY     });     canvas.dispatchEvent(mouseEvent); }  function handleTouchMove(e) {     e.preventDefault();     const touch = e.touches[0];     const mouseEvent = new MouseEvent('mousemove', {         clientX: touch.clientX,         clientY: touch.clientY     });     canvas.dispatchEvent(mouseEvent); }  function dragText(e) {   const rect = canvas.getBoundingClientRect();   const scaleX = canvas.width / rect.width;   const scaleY = canvas.height / rect.height;   const x = (e.clientX - rect.left) * scaleX;   const y = (e.clientY - rect.top) * scaleY;      // Update text position with offset   selectedTextElement.x = x + dragOffsetX;   selectedTextElement.y = y + dragOffsetY;      // Redraw selected text element   if (draggingCanvasContext) {     ctx.putImageData(draggingCanvasContext, 0, 0);   } else {     ctx.clearRect(0, 0, canvas.width, canvas.height);   }   ctx.font = selectedTextElement.font;   ctx.fillStyle = selectedTextElement.color;   ctx.fillText(selectedTextElement.text, selectedTextElement.x, selectedTextElement.y); }  function findTextElementAt(e) {   const rect = canvas.getBoundingClientRect();   const scaleX = canvas.width / rect.width;   const scaleY = canvas.height / rect.height;   const x = (e.clientX - rect.left) * scaleX;   const y = (e.clientY - rect.top) * scaleY;      // Search through text elements in reverse order (top-most first)   for (let i = textElements.length - 1; i >= 0; i--) {     const text = textElements[i];          // Calculate text dimensions     ctx.font = text.font;     const textWidth = ctx.measureText(text.text).width;      // Extract font size correctly from the font string     // This handles "bold 14px Arial", "italic 14px Arial", "bold italic 14px Arial", etc.     const fontSizeMatch = text.font.match(/(d+)px/);     const fontSize = fontSizeMatch ? parseInt(fontSizeMatch[1]) : 14; // Default to 14 if not found     const textHeight = fontSize * 1.2; // Approximate height          // Check if click is within text bounds (allowing for some margin)     const margin = 5;     if (x >= text.x - margin &&          x <= text.x + textWidth + margin &&          y >= text.y - textHeight + margin &&          y <= text.y + margin) {       return text;     }   }      return null; }  function startTextPlacement() {   const text = document.getElementById('text-input').value.trim();   if (!text) {       alert('请输入文字内容');       return;   }    isTextPlacementMode = true;    // Add visual feedback   setCanvasTitle('点击画布放置文字');   canvas.classList.add('text-placement-mode'); }  function cancelTextPlacement() {   isTextPlacementMode = false;   canvas.classList.remove('text-placement-mode');     // reset dragging state   isDraggingText = false;   dragOffsetX = 0;   dragOffsetY = 0;   selectedTextElement = null;   draggingCanvasContext = null; }  function placeText(e) {   const rect = canvas.getBoundingClientRect();   const scaleX = canvas.width / rect.width;   const scaleY = canvas.height / rect.height;   const x = (e.clientX - rect.left) * scaleX;   const y = (e.clientY - rect.top) * scaleY;    const text = document.getElementById('text-input').value;   const fontFamily = document.getElementById('font-family').value;   const fontSize = document.getElementById('font-size').value;    // Build font style string   let fontStyle = '';   if (textItalic) fontStyle += 'italic ';   if (textBold) fontStyle += 'bold ';      // Create a new text element   const newText = {     text: text,     x: x,     y: y,     font: `${fontStyle}${fontSize}px ${fontFamily}`,     color: brushColor   };      // Add to our list of text elements   textElements.push(newText);      // Select this text element for immediate dragging   selectedTextElement = newText;   draggingCanvasContext = ctx.getImageData(0, 0, canvas.width, canvas.height);      // Draw text on canvas   ctx.font = newText.font;   ctx.fillStyle = newText.color;   ctx.fillText(newText.text, newText.x, newText.y);      // Reset   document.getElementById('text-input').value = '';   isTextPlacementMode = false;   canvas.classList.remove('text-placement-mode');   setCanvasTitle('拖动新添加文字可调整位置'); }  function redrawTextElements() {   // Redraw all text elements after dithering   textElements.forEach(item => {     ctx.font = item.font;     ctx.fillStyle = item.color;     ctx.fillText(item.text, item.x, item.y);   }); }  function redrawLineSegments() {   // Redraw all line segments after dithering   lineSegments.forEach(segment => {     ctx.lineJoin = 'round';     ctx.lineCap = 'round';     ctx.strokeStyle = segment.color;     ctx.lineWidth = segment.size;     ctx.beginPath();          if (segment.type === 'dot') {       ctx.moveTo(segment.x, segment.y);       ctx.lineTo(segment.x+0.1, segment.y+0.1);     } else {       ctx.moveTo(segment.x1, segment.y1);       ctx.lineTo(segment.x2, segment.y2);     }          ctx.stroke();   }); } 
请登录后发表评论

    没有回复内容