套用tsl0922项目
Vercel Deploy / deploy (push) Successful in 1m16s

This commit is contained in:
biss committed 2026-04-25 10:21:38 +08:00
1 parent cc5d61af16
commit 0ce95688d5
15 files changed
+5016 -990

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
class CropManager {
constructor(canvas, ctx) {
this.canvas = canvas;
this.ctx = ctx;
this.backgroundZoom = 1;
this.backgroundPanX = 0;
this.backgroundPanY = 0;
this.isPanning = false;
this.lastPanX = 0;
this.lastPanY = 0;
this.lastTouchDistance = 0;
// Bind event handlers
this.handleBackgroundZoom = this.handleBackgroundZoom.bind(this);
this.handleBackgroundPanStart = this.handleBackgroundPanStart.bind(this);
this.handleBackgroundPan = this.handleBackgroundPan.bind(this);
this.handleBackgroundPanEnd = this.handleBackgroundPanEnd.bind(this);
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
}
resetStates() {
this.backgroundZoom = 1;
this.backgroundPanX = 0;
this.backgroundPanY = 0;
this.isPanning = false;
this.lastPanX = 0;
this.lastPanY = 0;
this.lastTouchDistance = 0;
}
isCropMode() {
return this.canvas.parentNode.classList.contains('crop-mode');
}
exitCropMode() {
this.canvas.parentNode.classList.remove('crop-mode');
setCanvasTitle("");
this.canvas.removeEventListener('wheel', this.handleBackgroundZoom);
this.canvas.removeEventListener('mousedown', this.handleBackgroundPanStart);
this.canvas.removeEventListener('mousemove', this.handleBackgroundPan);
this.canvas.removeEventListener('mouseup', this.handleBackgroundPanEnd);
this.canvas.removeEventListener('mouseleave', this.handleBackgroundPanEnd);
this.canvas.removeEventListener('touchstart', this.handleTouchStart);
this.canvas.removeEventListener('touchmove', this.handleTouchMove);
this.canvas.removeEventListener('touchend', this.handleBackgroundPanEnd);
this.canvas.removeEventListener('touchcancel', this.handleBackgroundPanEnd);
}
initializeCrop() {
const imageFile = document.getElementById('imageFile');
if (imageFile.files.length == 0) {
fillCanvas('white');
return;
}
this.exitCropMode();
this.resetStates();
this.canvas.style.backgroundImage = `url(${URL.createObjectURL(imageFile.files[0])})`;
this.canvas.style.backgroundSize = '100%';
this.canvas.style.backgroundPosition = '';
this.canvas.style.backgroundRepeat = 'no-repeat';
// add event listeners for zoom and pan
this.canvas.addEventListener('wheel', this.handleBackgroundZoom);
this.canvas.addEventListener('mousedown', this.handleBackgroundPanStart);
this.canvas.addEventListener('mousemove', this.handleBackgroundPan);
this.canvas.addEventListener('mouseup', this.handleBackgroundPanEnd);
this.canvas.addEventListener('mouseleave', this.handleBackgroundPanEnd);
// Touch events for mobile devices
this.canvas.addEventListener('touchstart', this.handleTouchStart);
this.canvas.addEventListener('touchmove', this.handleTouchMove);
this.canvas.addEventListener('touchend', this.handleBackgroundPanEnd);
this.canvas.addEventListener('touchcancel', this.handleBackgroundPanEnd);
// Make the canvas transparent
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
setCanvasTitle("裁剪模式: 可用鼠标滚轮或双指触摸缩放图片");
this.canvas.parentNode.classList.add('crop-mode');
}
finishCrop(callback) {
const imageFile = document.getElementById('imageFile');
if (imageFile.files.length == 0) return;
const image = new Image();
image.onload = () => {
URL.revokeObjectURL(image.src);
const fieldsetRect = this.canvas.getBoundingClientRect();
const scale = (image.width / fieldsetRect.width) / this.backgroundZoom;
const sx = -this.backgroundPanX * scale;
const sy = -this.backgroundPanY * scale;
const sWidth = fieldsetRect.width * scale;
const sHeight = fieldsetRect.height * scale;
fillCanvas('white');
this.ctx.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, this.canvas.width, this.canvas.height);
this.exitCropMode();
if (callback) callback();
};
image.src = URL.createObjectURL(imageFile.files[0]);
}
handleTouchStart(e) {
e.preventDefault();
if (e.touches.length === 1) {
this.handleBackgroundPanStart(e.touches[0]);
} else if (e.touches.length === 2) {
this.isPanning = false; // Stop panning when zooming
this.lastTouchDistance = this.getTouchDistance(e.touches);
}
}
handleTouchMove(e) {
e.preventDefault();
if (this.isPanning && e.touches.length === 1) {
this.handleBackgroundPan(e.touches[0]);
} else if (e.touches.length === 2) {
const newDist = this.getTouchDistance(e.touches);
if (this.lastTouchDistance > 0) {
const zoomFactor = newDist / this.lastTouchDistance;
this.backgroundZoom *= zoomFactor;
this.backgroundZoom = Math.max(0.1, Math.min(5, this.backgroundZoom)); // Limit zoom range
this.updateBackgroundTransform();
}
this.lastTouchDistance = newDist;
}
}
handleBackgroundZoom(e) {
e.preventDefault();
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
this.backgroundZoom *= zoomFactor;
this.backgroundZoom = Math.max(0.1, Math.min(5, this.backgroundZoom)); // Limit zoom range
this.updateBackgroundTransform();
}
handleBackgroundPanStart(e) {
this.isPanning = true;
this.lastPanX = e.clientX;
this.lastPanY = e.clientY;
this.canvas.style.cursor = 'grabbing';
}
handleBackgroundPan(e) {
if (this.isPanning) {
const deltaX = e.clientX - this.lastPanX;
const deltaY = e.clientY - this.lastPanY;
this.backgroundPanX += deltaX;
this.backgroundPanY += deltaY;
this.lastPanX = e.clientX;
this.lastPanY = e.clientY;
this.updateBackgroundTransform();
}
}
handleBackgroundPanEnd() {
this.isPanning = false;
this.lastTouchDistance = 0; // Reset touch distance
this.canvas.style.cursor = 'grab';
}
updateBackgroundTransform() {
this.canvas.style.backgroundSize = `${100 * this.backgroundZoom}%`;
this.canvas.style.backgroundPosition = `${this.backgroundPanX}px ${this.backgroundPanY}px`;
}
getTouchDistance(touches) {
const touch1 = touches[0];
const touch2 = touches[1];
return Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
);
}
initCropTools() {
document.getElementById('crop-zoom-in').addEventListener('click', (e) => {
e.preventDefault();
this.handleBackgroundZoom({ preventDefault: () => { }, deltaY: -1 });
});
document.getElementById('crop-zoom-out').addEventListener('click', (e) => {
e.preventDefault();
this.handleBackgroundZoom({ preventDefault: () => { }, deltaY: 1 });
});
document.getElementById('crop-move-left').addEventListener('click', (e) => {
e.preventDefault();
this.backgroundPanX -= 10;
this.updateBackgroundTransform();
});
document.getElementById('crop-move-right').addEventListener('click', (e) => {
e.preventDefault();
this.backgroundPanX += 10;
this.updateBackgroundTransform();
});
document.getElementById('crop-move-up').addEventListener('click', (e) => {
e.preventDefault();
this.backgroundPanY -= 10;
this.updateBackgroundTransform();
});
document.getElementById('crop-move-down').addEventListener('click', (e) => {
e.preventDefault();
this.backgroundPanY += 10;
this.updateBackgroundTransform();
});
}
}
+716
View File
@@ -0,0 +1,716 @@
// Ported from: https://e-paper-display.cn/usb2epd.html
// 固定的六色调色板
const rgbPalette = [
{ name: "黑色", r: 0, g: 0, b: 0, value: 0x00 },
{ name: "白色", r: 255, g: 255, b: 255, value: 0x01 },
{ name: "黄色", r: 255, g: 255, b: 0, value: 0x02 },
{ name: "红色", r: 255, g: 0, b: 0, value: 0x03 },
{ name: "蓝色", r: 0, g: 0, b: 255, value: 0x05 },
{ name: "绿色", r: 41, g: 204, b: 20, value: 0x06 }
];
// 四色调色板
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, mode) {
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[4]; // 蓝色
}
// 三色模式下优先检测红色
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, mode) {
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, mode);
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, mode);
data[idx] = closest.r;
data[idx + 1] = closest.g;
data[idx + 2] = closest.b;
}
}
return imageData;
}
function atkinsonDither(imageData, strength, mode) {
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, mode);
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, mode) {
// 执行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, mode);
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, mode);
data[idx] = closest.r;
data[idx + 1] = closest.g;
data[idx + 2] = closest.b;
}
}
return imageData;
}
function jarvisDither(imageData, strength, mode) {
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, mode);
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, mode) {
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, mode);
data[idx] = closest.r;
data[idx + 1] = closest.g;
data[idx + 2] = closest.b;
}
}
return imageData;
}
function ditherImage(imageData, alg, strength, mode) {
switch (alg) {
case 'floydSteinberg':
return floydSteinbergDither(imageData, strength, mode);
case 'atkinson':
return atkinsonDither(imageData, strength, mode);
case 'stucki':
return stuckiDither(imageData, strength, mode);
case 'jarvis':
return jarvisDither(imageData, strength, mode);
case 'bayer':
return bayerDither(imageData, strength, mode);
case 'none':
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[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; // 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, mode) {
const width = imageData.width;
const height = imageData.height;
const data = imageData.data;
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, mode);
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, mode); // 使用 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;
}
+1428
View File
File diff suppressed because it is too large. Load diff
Loaded 3 of 15 files, more files were not shown because too many files have changed in this diff. Show more