mirror of
https://github.com/tsl0922/EPD-nRF5.git
synced 2025-12-06 15:42:48 +08:00
update dithering algorithms
This commit is contained in:
@@ -20,13 +20,13 @@
|
||||
</div>
|
||||
<div class="flex-group debug">
|
||||
<label for="epddriver">驱动</label>
|
||||
<select id="epddriver" onchange="filterDitheringOptions()">
|
||||
<option value="01">UC8176/UC8276(黑白屏)</option>
|
||||
<option value="03">UC8176/UC8276(三色屏)</option>
|
||||
<option value="04">SSD1619/SSD1683(黑白屏)</option>
|
||||
<option value="02">SSD1619/SSD1683(三色屏)</option>
|
||||
<option value="05">JD79668(四色屏)</option>
|
||||
</select>
|
||||
<select id="epddriver" onchange="updateDitherMode()">
|
||||
<option value="01" data-color="blackWhiteColor">UC8176/UC8276(黑白)</option>
|
||||
<option value="03" data-color="threeColor">UC8176/UC8276(三色)</option>
|
||||
<option value="04" data-color="blackWhiteColor">SSD1619/SSD1683(黑白)</option>
|
||||
<option value="02" data-color="threeColor">SSD1619/SSD1683(三色)</option>
|
||||
<option value="05" data-color="fourColor">JD79668(四色)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-group debug">
|
||||
<label for="epdpins">引脚</label>
|
||||
@@ -52,29 +52,37 @@
|
||||
<input type="file" id="image_file" onchange="updateImage(true)" accept=".png,.jpg,.bmp,.webp,.jpeg">
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="flex-group">
|
||||
<label for="dithering">取模算法</label>
|
||||
<select id="dithering" title="取模算法" onchange="onDitheringChange()">
|
||||
<optgroup data-driver="01|04" label="黑白">
|
||||
<option value="none">二值化</option>
|
||||
<option value="bayer">bayer</option>
|
||||
<option value="floydsteinberg">floydsteinberg</option>
|
||||
<option value="Atkinson">Atkinson</option>
|
||||
</optgroup>
|
||||
<optgroup data-driver="02|03" label="三色">
|
||||
<option value="bwr_floydsteinberg">黑白红floydsteinberg</option>
|
||||
<option value="bwr_Atkinson">黑白红Atkinson</option>
|
||||
</optgroup>
|
||||
<optgroup data-driver="05" label="四色">
|
||||
<option value="bwry_floydsteinberg">黑白红黄floydsteinberg</option>
|
||||
<option value="bwry_Atkinson">黑白红黄Atkinson</option>
|
||||
</optgroup>
|
||||
<div class="flex-group debug">
|
||||
<label for="ditherMode">颜色模式:</label>
|
||||
<select id="ditherMode" onchange="updateImage(false)">
|
||||
<option value="blackWhiteColor">黑白</option>
|
||||
<option value="threeColor">三色</option>
|
||||
<option value="fourColor">四色</option>
|
||||
<option value="sixColor">六色</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-group">
|
||||
<label for="threshold">阈值</label>
|
||||
<input type="number" max="255" min="1" value="125" id="threshold" oninput="updateImage(false)">
|
||||
<label for="ditherType">抖动算法</label>
|
||||
<select id="ditherType" onchange="updateImage(false)">
|
||||
<option value="floydSteinberg">Floyd-Steinberg</option>
|
||||
<option value="atkinson">Atkinson</option>
|
||||
<option value="bayer">Bayer</option>
|
||||
<option value="stucki">Stucki</option>
|
||||
<option value="jarvis">Jarvis-Judice-Ninke</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="flex-group">
|
||||
<label for="ditherStrength">抖动强度</label>
|
||||
<input type="range" min="0" max="5" step="0.1" value="1.0" id="ditherStrength" oninput="updateImage(false)">
|
||||
</div>
|
||||
<div class="flex-group">
|
||||
<label for="contrast">对比度</label>
|
||||
<input type="range" min="0.5" max="2" step="0.1" value="1.2" id="contrast" oninput="updateImage(false)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="flex-group debug">
|
||||
<label for="mtusize">MTU</label>
|
||||
<input type="number" id="mtusize" value="20" min="0" max="255">
|
||||
@@ -86,6 +94,7 @@
|
||||
<div class="flex-container">
|
||||
<div class="flex-group">
|
||||
<button id="clearcanvasbutton" type="button" class="secondary" onclick="clearCanvas()">清除画布</button>
|
||||
<button id="downloadArray" type="button" class="secondary debug" onclick="downloadDataArray()">下载数组</button>
|
||||
<button id="sendimgbutton" type="button" class="primary" onclick="sendimg()">发送图片</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,6 +111,8 @@
|
||||
<option value="#000000">黑色</option>
|
||||
<option value="#FF0000">红色</option>
|
||||
<option value="#FFFF00">黄色</option>
|
||||
<option value="#00FF00">绿色</option>
|
||||
<option value="#0000FF">蓝色</option>
|
||||
<option value="#FFFFFF">白色</option>
|
||||
</select>
|
||||
<input type="number" id="brush-size" value="2" min="1" max="100" title="画笔大小">
|
||||
|
||||
@@ -1,194 +1,720 @@
|
||||
const bwPalette = [
|
||||
[0, 0, 0, 255], // black
|
||||
[255, 255, 255, 255] // white
|
||||
]
|
||||
// Ported from: https://e-paper-display.cn/usb2epd.html
|
||||
|
||||
const bwrPalette = [
|
||||
[0, 0, 0, 255], // black
|
||||
[255, 255, 255, 255], // white
|
||||
[255, 0, 0, 255] // red
|
||||
]
|
||||
// 固定的六色调色板
|
||||
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 bwryPalette = [
|
||||
[0, 0, 0, 255], // black
|
||||
[255, 255, 255, 255], // white
|
||||
[255, 255, 0, 255], // yellow
|
||||
[255, 0, 0, 255] // red
|
||||
]
|
||||
// 四色调色板
|
||||
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 }
|
||||
];
|
||||
|
||||
// black-white dithering
|
||||
function dithering(ctx, width, height, threshold, type) {
|
||||
const bayerThresholdMap = [
|
||||
[ 15, 135, 45, 165 ],
|
||||
[ 195, 75, 225, 105 ],
|
||||
[ 60, 180, 30, 150 ],
|
||||
[ 240, 120, 210, 90 ]
|
||||
// 三色调色板
|
||||
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 lumR = [];
|
||||
const lumG = [];
|
||||
const lumB = [];
|
||||
for (let i=0; i<256; i++) {
|
||||
lumR[i] = i*0.299;
|
||||
lumG[i] = i*0.587;
|
||||
lumB[i] = i*0.114;
|
||||
}
|
||||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
const imageDataLength = imageData.data.length;
|
||||
const matrixSize = 8;
|
||||
const maxThreshold = 64;
|
||||
|
||||
// Greyscale luminance (sets r pixels to luminance of rgb)
|
||||
for (let i = 0; i <= imageDataLength; i += 4) {
|
||||
imageData.data[i] = Math.floor(lumR[imageData.data[i]] + lumG[imageData.data[i+1]] + lumB[imageData.data[i+2]]);
|
||||
}
|
||||
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];
|
||||
|
||||
const w = imageData.width;
|
||||
let newPixel, err;
|
||||
// Get threshold from Bayer matrix
|
||||
const matrixX = x % matrixSize;
|
||||
const matrixY = y % matrixSize;
|
||||
const threshold = (bayerMatrix[matrixY][matrixX] / maxThreshold) * 255;
|
||||
|
||||
for (let currentPixel = 0; currentPixel <= imageDataLength; currentPixel+=4) {
|
||||
if (type ==="none") { // No dithering
|
||||
imageData.data[currentPixel] = imageData.data[currentPixel] < threshold ? 0 : 255;
|
||||
} else if (type ==="bayer") { // 4x4 Bayer ordered dithering algorithm
|
||||
var x = currentPixel/4 % w;
|
||||
var y = Math.floor(currentPixel/4 / w);
|
||||
var map = Math.floor( (imageData.data[currentPixel] + bayerThresholdMap[x%4][y%4]) / 2 );
|
||||
imageData.data[currentPixel] = (map < threshold) ? 0 : 255;
|
||||
} else if (type ==="floydsteinberg") { // Floyda€"Steinberg dithering algorithm
|
||||
newPixel = imageData.data[currentPixel] < threshold ? 0 : 255;
|
||||
err = Math.floor((imageData.data[currentPixel] - newPixel) / 16);
|
||||
imageData.data[currentPixel] = newPixel;
|
||||
imageData.data[currentPixel + 4 ] += err*7;
|
||||
imageData.data[currentPixel + 4*w - 4 ] += err*3;
|
||||
imageData.data[currentPixel + 4*w ] += err*5;
|
||||
imageData.data[currentPixel + 4*w + 4 ] += err*1;
|
||||
} else { // Bill Atkinson's dithering algorithm
|
||||
newPixel = imageData.data[currentPixel] < threshold ? 0 : 255;
|
||||
err = Math.floor((imageData.data[currentPixel] - newPixel) / 8);
|
||||
imageData.data[currentPixel] = newPixel;
|
||||
imageData.data[currentPixel + 4 ] += err;
|
||||
imageData.data[currentPixel + 8 ] += err;
|
||||
imageData.data[currentPixel + 4*w - 4 ] += err;
|
||||
imageData.data[currentPixel + 4*w ] += err;
|
||||
imageData.data[currentPixel + 4*w + 4 ] += err;
|
||||
imageData.data[currentPixel + 8*w ] += err;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Set g and b pixels equal to r
|
||||
imageData.data[currentPixel + 1] = imageData.data[currentPixel + 2] = imageData.data[currentPixel];
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return imageData;
|
||||
}
|
||||
|
||||
function canvas2bytes(canvas, step = 'bw', invert = false) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
function ditherImage(imageData) {
|
||||
const ditherType = document.getElementById('ditherType').value;
|
||||
const ditherStrength = parseFloat(document.getElementById('ditherStrength').value);
|
||||
|
||||
const arr = [];
|
||||
let buffer = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 0; y < canvas.height; y++) {
|
||||
for (let x = 0; x < canvas.width; x++) {
|
||||
const i = (canvas.width * y + x) * 4;
|
||||
function decodeProcessedData(processedData, width, height, mode) {
|
||||
const imageData = new ImageData(width, height);
|
||||
const data = imageData.data;
|
||||
|
||||
const r = imageData.data[i];
|
||||
const g = imageData.data[i + 1];
|
||||
const b = imageData.data[i + 2];
|
||||
|
||||
if (step === 'bwry') { // black: 0, white: 1, yellow: 2, red: 3
|
||||
buffer.push(getNearColorIdx([r, g, b, 255], bwryPalette));
|
||||
if (buffer.length === 4) {
|
||||
const byte = (buffer[0] << 6) | (buffer[1] << 4) | (buffer[2] << 2) | buffer[3];
|
||||
arr.push(invert ? ~byte & 0xFF : byte);
|
||||
buffer = [];
|
||||
}
|
||||
} else { // white: 1, black/red: 0
|
||||
if (step === 'bw') {
|
||||
buffer.push(r === 0 && g === 0 && b === 0 ? 0 : 1);
|
||||
} else if (step === 'red') {
|
||||
buffer.push(r > 0 && g === 0 && b === 0 ? 0 : 1);
|
||||
}
|
||||
if (buffer.length === 8) {
|
||||
const data = parseInt(buffer.join(''), 2);
|
||||
arr.push(invert ? ~data : data);
|
||||
buffer = [];
|
||||
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 arr;
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
function getNearColorIdx(color, palette) {
|
||||
let minDistanceSquared = 255*255 + 255*255 + 255*255 + 1;
|
||||
let bestIndex = 0;
|
||||
for (let i = 0; i < palette.length; i++) {
|
||||
let rdiff = (color[0] & 0xff) - (palette[i][0] & 0xff);
|
||||
let gdiff = (color[1] & 0xff) - (palette[i][1] & 0xff);
|
||||
let bdiff = (color[2] & 0xff) - (palette[i][2] & 0xff);
|
||||
let distanceSquared = rdiff*rdiff + gdiff*gdiff + bdiff*bdiff;
|
||||
if (distanceSquared < minDistanceSquared) {
|
||||
minDistanceSquared = distanceSquared;
|
||||
bestIndex = i;
|
||||
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;
|
||||
}
|
||||
}
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
function updatePixel(imageData, index, color) {
|
||||
imageData[index] = color[0];
|
||||
imageData[index+1] = color[1];
|
||||
imageData[index+2] = color[2];
|
||||
imageData[index+3] = color[3];
|
||||
}
|
||||
|
||||
function getColorErr(color1, color2, rate) {
|
||||
const res = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
res.push(Math.floor((color1[i] - color2[i]) / rate));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function updatePixelErr(imageData, index, err, rate) {
|
||||
imageData[index] += err[0] * rate;
|
||||
imageData[index+1] += err[1] * rate;
|
||||
imageData[index+2] += err[2] * rate;
|
||||
}
|
||||
|
||||
// Dithering by palette
|
||||
function ditheringByPalette(canvas, type) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const w = imageData.width;
|
||||
let palette = bwPalette;
|
||||
|
||||
if (type.startsWith("bwry")) {
|
||||
palette = bwryPalette;
|
||||
} else if (type.startsWith("bwr")) {
|
||||
palette = bwrPalette;
|
||||
}
|
||||
|
||||
for (let currentPixel = 0; currentPixel <= imageData.data.length; currentPixel+=4) {
|
||||
const color = imageData.data.slice(currentPixel, currentPixel+4);
|
||||
const newColor = palette[getNearColorIdx(color, palette)];
|
||||
|
||||
if (type.endsWith("floydsteinberg")) {
|
||||
const err = getColorErr(color, newColor, 16);
|
||||
updatePixel(imageData.data, currentPixel, newColor);
|
||||
updatePixelErr(imageData.data, currentPixel +4, err, 7);
|
||||
updatePixelErr(imageData.data, currentPixel + 4*w - 4, err, 3);
|
||||
updatePixelErr(imageData.data, currentPixel + 4*w, err, 5);
|
||||
updatePixelErr(imageData.data, currentPixel + 4*w + 4, err, 1);
|
||||
} else {
|
||||
const err = getColorErr(color, newColor, 8);
|
||||
updatePixel(imageData.data, currentPixel, newColor);
|
||||
updatePixelErr(imageData.data, currentPixel +4, err, 1);
|
||||
updatePixelErr(imageData.data, currentPixel +8, err, 1);
|
||||
updatePixelErr(imageData.data, currentPixel +4 * w - 4, err, 1);
|
||||
updatePixelErr(imageData.data, currentPixel +4 * w, err, 1);
|
||||
updatePixelErr(imageData.data, currentPixel +4 * w + 4, err, 1);
|
||||
updatePixelErr(imageData.data, currentPixel +8 * w, err, 1);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
return processedData;
|
||||
}
|
||||
|
||||
220
html/js/main.js
220
html/js/main.js
@@ -48,7 +48,7 @@ function resetVariables() {
|
||||
document.getElementById("log").value = '';
|
||||
}
|
||||
|
||||
async function write(cmd, data, withResponse=true) {
|
||||
async function write(cmd, data, withResponse = true) {
|
||||
if (!epdCharacteristic) {
|
||||
addLog("服务不可用,请检查蓝牙连接");
|
||||
return false;
|
||||
@@ -85,7 +85,7 @@ async function epdWrite(cmd, data) {
|
||||
await write(EpdCmd.SEND_CMD, [cmd]);
|
||||
for (let i = 0; i < data.length; i += chunkSize) {
|
||||
let currentTime = (new Date().getTime() - startTime) / 1000.0;
|
||||
setStatus(`命令:0x${cmd.toString(16)}, 数据块: ${chunkIdx+1}/${count+1}, 总用时: ${currentTime}s`);
|
||||
setStatus(`命令:0x${cmd.toString(16)}, 数据块: ${chunkIdx + 1}/${count + 1}, 总用时: ${currentTime}s`);
|
||||
if (noReplyCount > 0) {
|
||||
await write(EpdCmd.SEND_DATA, data.slice(i, i + chunkSize), false);
|
||||
noReplyCount--;
|
||||
@@ -97,8 +97,7 @@ async function epdWrite(cmd, data) {
|
||||
}
|
||||
}
|
||||
|
||||
async function epdWriteImage(step = 'bw') {
|
||||
const data = canvas2bytes(canvas, step);
|
||||
async function epdWriteImage(data, step = 'bw') {
|
||||
const chunkSize = document.getElementById('mtusize').value - 2;
|
||||
const interleavedCount = document.getElementById('interleavedcount').value;
|
||||
const count = Math.round(data.length / chunkSize);
|
||||
@@ -107,9 +106,9 @@ async function epdWriteImage(step = 'bw') {
|
||||
|
||||
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`);
|
||||
setStatus(`${step == 'bw' ? '黑白' : '颜色'}块: ${chunkIdx + 1}/${count + 1}, 总用时: ${currentTime}s`);
|
||||
const payload = [
|
||||
(step == 'bw' ? 0x0F : 0x00) | ( i == 0 ? 0x00 : 0xF0),
|
||||
(step == 'bw' ? 0x0F : 0x00) | (i == 0 ? 0x00 : 0xF0),
|
||||
...data.slice(i, i + chunkSize),
|
||||
];
|
||||
if (noReplyCount > 0) {
|
||||
@@ -138,14 +137,14 @@ async function syncTime(mode) {
|
||||
-(new Date().getTimezoneOffset() / 60),
|
||||
mode
|
||||
]);
|
||||
if(await write(EpdCmd.SET_TIME, data)) {
|
||||
if (await write(EpdCmd.SET_TIME, data)) {
|
||||
addLog("时间已同步!");
|
||||
addLog("屏幕刷新完成前请不要操作。");
|
||||
}
|
||||
}
|
||||
|
||||
async function clearScreen() {
|
||||
if(confirm('确认清除屏幕内容?')) {
|
||||
if (confirm('确认清除屏幕内容?')) {
|
||||
await write(EpdCmd.CLEAR);
|
||||
addLog("清屏指令已发送!");
|
||||
addLog("屏幕刷新完成前请不要操作。");
|
||||
@@ -160,35 +159,52 @@ async function sendcmd() {
|
||||
}
|
||||
|
||||
async function sendimg() {
|
||||
const status = document.getElementById("status");
|
||||
const driver = document.getElementById("epddriver").value;
|
||||
const mode = document.getElementById('dithering').value;
|
||||
|
||||
startTime = new Date().getTime();
|
||||
status.parentElement.style.display = "block";
|
||||
|
||||
updateButtonStatus(true);
|
||||
if (appVersion < 0x16) {
|
||||
if (mode.startsWith('bwry')) {
|
||||
addLog("当前固件版本不支持四色屏幕。");
|
||||
return;
|
||||
} if (mode.startsWith('bwr')) {
|
||||
await epdWrite(driver === "02" ? 0x24 : 0x10, canvas2bytes(canvas, 'bw'));
|
||||
await epdWrite(driver === "02" ? 0x26 : 0x13, canvas2bytes(canvas, 'red', driver === '02'));
|
||||
} else {
|
||||
await epdWrite(driver === "04" ? 0x24 : 0x13, canvas2bytes(canvas, 'bw'));
|
||||
}
|
||||
} else {
|
||||
if (mode.startsWith('bwry')) {
|
||||
await epdWriteImage('bwry');
|
||||
} else {
|
||||
await epdWriteImage('bw');
|
||||
if (mode.startsWith('bwr')) await epdWriteImage('red');
|
||||
}
|
||||
const ditherMode = document.getElementById('ditherMode').value;
|
||||
const epdDriverSelect = document.getElementById('epddriver');
|
||||
const selectedOption = epdDriverSelect.options[epdDriverSelect.selectedIndex];
|
||||
if (selectedOption.getAttribute('data-color') !== ditherMode) {
|
||||
addLog(`颜色模式和驱动不匹配,请重新选择。`);
|
||||
return;
|
||||
}
|
||||
|
||||
await write(EpdCmd.REFRESH);
|
||||
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);
|
||||
|
||||
let dataSent = true;
|
||||
updateButtonStatus(true);
|
||||
if (appVersion < 0x16) {
|
||||
if (ditherMode === 'threeColor') {
|
||||
const halfLength = Math.floor(processedData.length / 2);
|
||||
await epdWrite(driver === "02" ? 0x24 : 0x10, processedData.slice(0, halfLength));
|
||||
await epdWrite(driver === "02" ? 0x26 : 0x13, processedData.slice(halfLength));
|
||||
} else if (ditherMode === 'blackWhiteColor') {
|
||||
await epdWrite(driver === "04" ? 0x24 : 0x13, processedData);
|
||||
} else {
|
||||
addLog("当前固件不支持此颜色模式。");
|
||||
dataSent = false;
|
||||
}
|
||||
} else {
|
||||
if (ditherMode === 'fourColor') {
|
||||
await epdWriteImage(processedData, 'color');
|
||||
} else if (ditherMode === 'threeColor') {
|
||||
const halfLength = Math.floor(processedData.length / 2);
|
||||
await epdWriteImage(processedData.slice(0, halfLength), 'bw');
|
||||
await epdWriteImage(processedData.slice(halfLength), 'red');
|
||||
} else if (ditherMode === 'blackWhiteColor') {
|
||||
await epdWriteImage(processedData, 'bw');
|
||||
} else {
|
||||
addLog("当前固件不支持此颜色模式。");
|
||||
dataSent = false;
|
||||
}
|
||||
}
|
||||
updateButtonStatus();
|
||||
if (!dataSent) return;
|
||||
|
||||
await write(EpdCmd.REFRESH);
|
||||
|
||||
const sendTime = (new Date().getTime() - startTime) / 1000.0;
|
||||
addLog(`发送完成!耗时: ${sendTime}s`);
|
||||
@@ -199,6 +215,50 @@ async function sendimg() {
|
||||
}, 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}`);
|
||||
}
|
||||
|
||||
// 每行格式化16个值
|
||||
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 = '图像数据.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');
|
||||
@@ -263,7 +323,7 @@ function handleNotify(value, idx) {
|
||||
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));
|
||||
filterDitheringOptions();
|
||||
updateDitherMode();
|
||||
} else {
|
||||
if (textDecoder == null) textDecoder = new TextDecoder();
|
||||
addLog(textDecoder.decode(data), '⇓');
|
||||
@@ -322,15 +382,15 @@ 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') + " ";
|
||||
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';
|
||||
@@ -341,7 +401,7 @@ function addLog(logTXT, action = '') {
|
||||
|
||||
log.appendChild(logEntry);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
|
||||
|
||||
while (log.childNodes.length > 20) {
|
||||
log.removeChild(log.firstChild);
|
||||
}
|
||||
@@ -351,11 +411,15 @@ function clearLog() {
|
||||
document.getElementById("log").innerHTML = '';
|
||||
}
|
||||
|
||||
function onDitheringChange() {
|
||||
const mode = document.getElementById('dithering').value;
|
||||
const thresholdInput = document.getElementById('threshold');
|
||||
thresholdInput.disabled = (mode === '' || mode.startsWith('bwr'));
|
||||
updateImage(false);
|
||||
function updateDitherMode() {
|
||||
const epdDriverSelect = document.getElementById('epddriver');
|
||||
const selectedOption = epdDriverSelect.options[epdDriverSelect.selectedIndex];
|
||||
const colorMode = selectedOption.getAttribute('data-color');
|
||||
|
||||
if (colorMode) {
|
||||
document.getElementById('ditherMode').value = colorMode;
|
||||
updateImage(false);
|
||||
}
|
||||
}
|
||||
|
||||
function updateImage(clear = false) {
|
||||
@@ -367,9 +431,14 @@ function updateImage(clear = false) {
|
||||
const file = image_file.files[0];
|
||||
let image = new Image();;
|
||||
image.src = URL.createObjectURL(file);
|
||||
image.onload = function(event) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -386,57 +455,36 @@ function clearCanvas() {
|
||||
}
|
||||
|
||||
function convertDithering() {
|
||||
const mode = document.getElementById('dithering').value;
|
||||
if (mode === '') return;
|
||||
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
|
||||
);
|
||||
|
||||
if (mode.startsWith('bwr')) {
|
||||
ditheringByPalette(canvas, mode);
|
||||
} else {
|
||||
const threshold = document.getElementById('threshold').value;
|
||||
dithering(ctx, canvas.width, canvas.height, parseInt(threshold), mode);
|
||||
}
|
||||
adjustContrast(imageData, contrast);
|
||||
|
||||
// Redraw text and lines
|
||||
redrawTextElements();
|
||||
redrawLineSegments();
|
||||
}
|
||||
|
||||
function filterDitheringOptions() {
|
||||
const driver = document.getElementById('epddriver').value;
|
||||
const dithering = document.getElementById('dithering');
|
||||
let currentOptionStillValid = false;
|
||||
let lastValidOptionValue = null;
|
||||
|
||||
for (let optgroup of dithering.getElementsByTagName('optgroup')) {
|
||||
const drivers = optgroup.getAttribute('data-driver').split('|');
|
||||
const show = drivers.includes(driver);
|
||||
for (option of optgroup.getElementsByTagName('option')) {
|
||||
if (show) {
|
||||
option.removeAttribute('disabled');
|
||||
if (option.value == dithering.value) currentOptionStillValid = true;
|
||||
lastValidOptionValue = option.value;
|
||||
} else {
|
||||
option.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!currentOptionStillValid) dithering.value = lastValidOptionValue;
|
||||
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("注意:开发模式功能已开启!不懂请不要随意修改,否则后果自负!");
|
||||
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.classList.remove('debug-mode');
|
||||
link.innerHTML = '开发模式';
|
||||
link.setAttribute('href', window.location.pathname + '?debug=true');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,12 +74,8 @@ function initPaintTools() {
|
||||
|
||||
setupCanvasForPainting();
|
||||
|
||||
// Update the brush color options based on dithering method
|
||||
document.getElementById('dithering').addEventListener('change', updateBrushOptions);
|
||||
|
||||
// Ensure no tool is selected by default
|
||||
updateToolUI();
|
||||
updateBrushOptions();
|
||||
}
|
||||
|
||||
function setActiveTool(tool, title) {
|
||||
@@ -92,28 +88,6 @@ function setActiveTool(tool, title) {
|
||||
cancelTextPlacement();
|
||||
}
|
||||
|
||||
function updateBrushOptions() {
|
||||
const dithering = document.getElementById('dithering').value;
|
||||
const brushColor = document.getElementById('brush-color');
|
||||
for (option of brushColor.getElementsByTagName('option')) {
|
||||
if (option.value === '#FF0000') {
|
||||
if (dithering.startsWith('bwr'))
|
||||
option.removeAttribute('disabled');
|
||||
else
|
||||
option.setAttribute('disabled', 'disabled');
|
||||
} else if (option.value === '#FFFF00') {
|
||||
if (dithering.startsWith('bwry'))
|
||||
option.removeAttribute('disabled');
|
||||
else
|
||||
option.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
// Revert brush color to black if red is not allowed
|
||||
if (!dithering.startsWith('bwr') && brushColor.value === '#FF0000') {
|
||||
brushColor.value = '#000000';
|
||||
}
|
||||
}
|
||||
|
||||
function updateToolUI() {
|
||||
// Update UI to reflect active tool or no tool
|
||||
document.getElementById('brush-mode').classList.toggle('active', currentTool === 'brush');
|
||||
|
||||
Reference in New Issue
Block a user