+219
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+631
@@ -0,0 +1,631 @@
|
||||
class PaintManager {
|
||||
constructor(canvas, ctx) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = ctx;
|
||||
this.painting = false;
|
||||
this.lastX = 0;
|
||||
this.lastY = 0;
|
||||
this.brushColor = "#000000";
|
||||
this.brushSize = 2;
|
||||
this.currentTool = null;
|
||||
this.textElements = [];
|
||||
this.lineSegments = [];
|
||||
this.isTextPlacementMode = false;
|
||||
this.draggingCanvasContext = null;
|
||||
this.selectedTextElement = null;
|
||||
this.isDraggingText = false;
|
||||
this.dragOffsetX = 0;
|
||||
this.dragOffsetY = 0;
|
||||
this.textBold = false;
|
||||
this.textItalic = false;
|
||||
|
||||
// Brush cursor indicator
|
||||
this.brushCursor = null;
|
||||
|
||||
// Undo/Redo functionality
|
||||
this.historyStack = [];
|
||||
this.historyStep = -1;
|
||||
this.MAX_HISTORY = 50;
|
||||
|
||||
// Bind event handlers
|
||||
this.startPaint = this.startPaint.bind(this);
|
||||
this.paint = this.paint.bind(this);
|
||||
this.endPaint = this.endPaint.bind(this);
|
||||
this.handleCanvasClick = this.handleCanvasClick.bind(this);
|
||||
this.onTouchStart = this.onTouchStart.bind(this);
|
||||
this.onTouchMove = this.onTouchMove.bind(this);
|
||||
this.onTouchEnd = this.onTouchEnd.bind(this);
|
||||
this.handleKeyboard = this.handleKeyboard.bind(this);
|
||||
this.updateBrushCursor = this.updateBrushCursor.bind(this);
|
||||
this.hideBrushCursor = this.hideBrushCursor.bind(this);
|
||||
}
|
||||
|
||||
saveToHistory() {
|
||||
// Remove any states after current step (when user drew something after undoing)
|
||||
this.historyStack = this.historyStack.slice(0, this.historyStep + 1);
|
||||
|
||||
// Save current canvas state along with text and line data
|
||||
const canvasState = {
|
||||
imageData: this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height),
|
||||
textElements: JSON.parse(JSON.stringify(this.textElements)),
|
||||
lineSegments: JSON.parse(JSON.stringify(this.lineSegments))
|
||||
};
|
||||
|
||||
this.historyStack.push(canvasState);
|
||||
this.historyStep++;
|
||||
|
||||
// Limit history size
|
||||
if (this.historyStack.length > this.MAX_HISTORY) {
|
||||
this.historyStack.shift();
|
||||
this.historyStep--;
|
||||
}
|
||||
|
||||
this.updateUndoRedoButtons();
|
||||
}
|
||||
|
||||
clearHistory() {
|
||||
this.historyStack = [];
|
||||
this.historyStep = -1;
|
||||
this.updateUndoRedoButtons();
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.historyStep > 0) {
|
||||
this.historyStep--;
|
||||
this.restoreFromHistory();
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (this.historyStep < this.historyStack.length - 1) {
|
||||
this.historyStep++;
|
||||
this.restoreFromHistory();
|
||||
}
|
||||
}
|
||||
|
||||
restoreFromHistory() {
|
||||
if (this.historyStep >= 0 && this.historyStep < this.historyStack.length) {
|
||||
const state = this.historyStack[this.historyStep];
|
||||
|
||||
// Restore canvas image
|
||||
this.ctx.putImageData(state.imageData, 0, 0);
|
||||
|
||||
// Restore text and line data
|
||||
this.textElements = JSON.parse(JSON.stringify(state.textElements));
|
||||
this.lineSegments = JSON.parse(JSON.stringify(state.lineSegments));
|
||||
|
||||
this.updateUndoRedoButtons();
|
||||
}
|
||||
}
|
||||
|
||||
updateUndoRedoButtons() {
|
||||
const undoBtn = document.getElementById('undo-btn');
|
||||
const redoBtn = document.getElementById('redo-btn');
|
||||
|
||||
if (undoBtn) {
|
||||
undoBtn.disabled = this.historyStep <= 0;
|
||||
}
|
||||
|
||||
if (redoBtn) {
|
||||
redoBtn.disabled = this.historyStep >= this.historyStack.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
initPaintTools() {
|
||||
document.getElementById('brush-mode').addEventListener('click', () => {
|
||||
if (this.currentTool === 'brush') {
|
||||
this.setActiveTool(null, '');
|
||||
} else {
|
||||
this.setActiveTool('brush', '画笔模式');
|
||||
this.brushColor = document.getElementById('brush-color').value;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('eraser-mode').addEventListener('click', () => {
|
||||
if (this.currentTool === 'eraser') {
|
||||
this.setActiveTool(null, '');
|
||||
} else {
|
||||
this.setActiveTool('eraser', '橡皮擦');
|
||||
this.brushColor = "#FFFFFF";
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('text-mode').addEventListener('click', () => {
|
||||
if (this.currentTool === 'text') {
|
||||
this.setActiveTool(null, '');
|
||||
} else {
|
||||
this.setActiveTool('text', '插入文字');
|
||||
this.brushColor = document.getElementById('brush-color').value;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('brush-color').addEventListener('change', (e) => {
|
||||
this.brushColor = e.target.value;
|
||||
});
|
||||
|
||||
document.getElementById('brush-size').addEventListener('input', (e) => {
|
||||
this.brushSize = parseInt(e.target.value);
|
||||
this.updateBrushCursorSize();
|
||||
});
|
||||
|
||||
document.getElementById('add-text-btn').addEventListener('click', () => this.startTextPlacement());
|
||||
|
||||
// Add event listeners for bold and italic buttons
|
||||
document.getElementById('text-bold').addEventListener('click', () => {
|
||||
this.textBold = !this.textBold;
|
||||
document.getElementById('text-bold').classList.toggle('primary', this.textBold);
|
||||
});
|
||||
|
||||
document.getElementById('text-italic').addEventListener('click', () => {
|
||||
this.textItalic = !this.textItalic;
|
||||
document.getElementById('text-italic').classList.toggle('primary', this.textItalic);
|
||||
});
|
||||
|
||||
// Add undo/redo button listeners
|
||||
document.getElementById('undo-btn').addEventListener('click', () => this.undo());
|
||||
document.getElementById('redo-btn').addEventListener('click', () => this.redo());
|
||||
|
||||
this.canvas.addEventListener('mousedown', this.startPaint);
|
||||
this.canvas.addEventListener('mousemove', this.paint);
|
||||
this.canvas.addEventListener('mouseup', this.endPaint);
|
||||
this.canvas.addEventListener('mouseleave', this.endPaint);
|
||||
this.canvas.addEventListener('click', this.handleCanvasClick);
|
||||
|
||||
// Touch support
|
||||
this.canvas.addEventListener('touchstart', this.onTouchStart);
|
||||
this.canvas.addEventListener('touchmove', this.onTouchMove);
|
||||
this.canvas.addEventListener('touchend', this.onTouchEnd);
|
||||
|
||||
// Keyboard shortcuts for undo/redo
|
||||
document.addEventListener('keydown', this.handleKeyboard);
|
||||
|
||||
// Mouse move for brush cursor
|
||||
this.canvas.addEventListener('mouseenter', this.updateBrushCursor);
|
||||
this.canvas.addEventListener('mousemove', this.updateBrushCursor);
|
||||
|
||||
// Create brush cursor element
|
||||
this.createBrushCursor();
|
||||
|
||||
// Initialize history with blank canvas state
|
||||
this.saveToHistory();
|
||||
}
|
||||
|
||||
handleKeyboard(e) {
|
||||
// Ctrl+Z or Cmd+Z for undo
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.undo();
|
||||
}
|
||||
// Ctrl+Y or Ctrl+Shift+Z or Cmd+Shift+Z for redo
|
||||
else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.shiftKey && e.key === 'z'))) {
|
||||
e.preventDefault();
|
||||
this.redo();
|
||||
}
|
||||
}
|
||||
|
||||
setActiveTool(tool, title) {
|
||||
setCanvasTitle(title);
|
||||
this.currentTool = tool;
|
||||
|
||||
this.canvas.parentNode.classList.toggle('brush-mode', this.currentTool === 'brush');
|
||||
this.canvas.parentNode.classList.toggle('eraser-mode', this.currentTool === 'eraser');
|
||||
this.canvas.parentNode.classList.toggle('text-mode', this.currentTool === 'text');
|
||||
|
||||
document.getElementById('brush-mode').classList.toggle('active', this.currentTool === 'brush');
|
||||
document.getElementById('eraser-mode').classList.toggle('active', this.currentTool === 'eraser');
|
||||
document.getElementById('text-mode').classList.toggle('active', this.currentTool === 'text');
|
||||
|
||||
document.getElementById('brush-color').disabled = this.currentTool === 'eraser';
|
||||
document.getElementById('brush-size').disabled = this.currentTool === 'text';
|
||||
|
||||
document.getElementById('undo-btn').classList.toggle('hide', this.currentTool === null);
|
||||
document.getElementById('redo-btn').classList.toggle('hide', this.currentTool === null);
|
||||
|
||||
// Cancel any pending text placement
|
||||
this.cancelTextPlacement();
|
||||
}
|
||||
|
||||
createBrushCursor() {
|
||||
// Create a div element to show as brush cursor
|
||||
this.brushCursor = document.createElement('div');
|
||||
this.brushCursor.id = 'brush-cursor';
|
||||
this.brushCursor.style.position = 'fixed';
|
||||
this.brushCursor.style.border = '2px solid rgba(0, 0, 0, 0.5)';
|
||||
this.brushCursor.style.borderRadius = '50%';
|
||||
this.brushCursor.style.pointerEvents = 'none';
|
||||
this.brushCursor.style.display = 'none';
|
||||
this.brushCursor.style.zIndex = '10000';
|
||||
this.brushCursor.style.transform = 'translate(-50%, -50%)';
|
||||
this.brushCursor.style.willChange = 'transform';
|
||||
this.brushCursor.style.left = '0';
|
||||
this.brushCursor.style.top = '0';
|
||||
document.body.appendChild(this.brushCursor);
|
||||
this.updateBrushCursorSize();
|
||||
|
||||
// For requestAnimationFrame throttling
|
||||
this.cursorUpdateScheduled = false;
|
||||
this.pendingCursorX = 0;
|
||||
this.pendingCursorY = 0;
|
||||
}
|
||||
|
||||
updateBrushCursorSize() {
|
||||
if (!this.brushCursor) return;
|
||||
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = rect.width / this.canvas.width;
|
||||
const scaleY = rect.height / this.canvas.height;
|
||||
const scale = Math.min(scaleX, scaleY);
|
||||
|
||||
const size = this.brushSize * scale;
|
||||
this.brushCursor.style.width = size + 'px';
|
||||
this.brushCursor.style.height = size + 'px';
|
||||
}
|
||||
|
||||
updateBrushCursor(e) {
|
||||
if (!this.brushCursor) return;
|
||||
|
||||
if (this.currentTool === 'brush' || this.currentTool === 'eraser') {
|
||||
// Check if mouse is within canvas bounds
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const isInCanvas = e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= rect.bottom;
|
||||
|
||||
if (isInCanvas) {
|
||||
this.brushCursor.style.display = 'block';
|
||||
this.canvas.style.cursor = 'none';
|
||||
|
||||
// Store the pending position
|
||||
this.pendingCursorX = e.clientX;
|
||||
this.pendingCursorY = e.clientY;
|
||||
|
||||
// Schedule update using requestAnimationFrame for smooth movement
|
||||
if (!this.cursorUpdateScheduled) {
|
||||
this.cursorUpdateScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.brushCursor.style.transform = `translate(${this.pendingCursorX}px, ${this.pendingCursorY}px) translate(-50%, -50%)`;
|
||||
this.cursorUpdateScheduled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Update color to match brush or show white for eraser (only needs to happen once or when tool changes)
|
||||
if (this.currentTool === 'eraser') {
|
||||
if (this.brushCursor.getAttribute('data-tool') !== 'eraser') {
|
||||
this.brushCursor.style.border = '2px solid rgba(255, 0, 0, 0.7)';
|
||||
this.brushCursor.style.backgroundColor = 'rgba(255, 255, 255, 0.2)';
|
||||
this.brushCursor.style.boxShadow = 'none';
|
||||
this.brushCursor.setAttribute('data-tool', 'eraser');
|
||||
}
|
||||
} else {
|
||||
if (this.brushCursor.getAttribute('data-tool') !== 'brush') {
|
||||
// Use a contrasting border - white with black outline for visibility
|
||||
this.brushCursor.style.border = '1px solid white';
|
||||
this.brushCursor.style.boxShadow = '0 0 0 1px black, inset 0 0 0 1px black';
|
||||
this.brushCursor.style.backgroundColor = 'transparent';
|
||||
this.brushCursor.setAttribute('data-tool', 'brush');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Hide cursor when outside canvas
|
||||
this.brushCursor.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideBrushCursor() {
|
||||
if (this.brushCursor) {
|
||||
this.brushCursor.style.display = 'none';
|
||||
}
|
||||
this.canvas.style.cursor = 'default';
|
||||
}
|
||||
|
||||
startPaint(e) {
|
||||
if (!this.currentTool) return;
|
||||
|
||||
if (this.currentTool === 'text') {
|
||||
// Check if we're clicking on a text element to drag
|
||||
const textElement = this.findTextElementAt(e);
|
||||
if (textElement && textElement === this.selectedTextElement) {
|
||||
this.isDraggingText = true;
|
||||
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / rect.width;
|
||||
const scaleY = this.canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Calculate offset for smooth dragging
|
||||
this.dragOffsetX = textElement.x - x;
|
||||
this.dragOffsetY = textElement.y - y;
|
||||
|
||||
return; // Don't start drawing
|
||||
}
|
||||
} else {
|
||||
this.painting = true;
|
||||
this.draw(e);
|
||||
}
|
||||
}
|
||||
|
||||
endPaint() {
|
||||
if (this.painting || this.isDraggingText) {
|
||||
this.saveToHistory(); // Save state after drawing or dragging text
|
||||
}
|
||||
this.painting = false;
|
||||
this.isDraggingText = false;
|
||||
this.lastX = 0;
|
||||
this.lastY = 0;
|
||||
|
||||
this.hideBrushCursor();
|
||||
}
|
||||
|
||||
paint(e) {
|
||||
if (!this.currentTool) return;
|
||||
|
||||
if (this.currentTool === 'text') {
|
||||
if (this.isDraggingText && this.selectedTextElement) {
|
||||
this.dragText(e);
|
||||
}
|
||||
} else {
|
||||
if (this.painting) {
|
||||
this.draw(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / rect.width;
|
||||
const scaleY = this.canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
this.ctx.lineJoin = 'round';
|
||||
this.ctx.lineCap = 'round';
|
||||
this.ctx.strokeStyle = this.brushColor;
|
||||
this.ctx.lineWidth = this.brushSize;
|
||||
|
||||
this.ctx.beginPath();
|
||||
|
||||
if (this.lastX === 0 && this.lastY === 0) {
|
||||
// For the first point, just do a dot
|
||||
this.ctx.moveTo(x, y);
|
||||
this.ctx.lineTo(x + 0.1, y + 0.1);
|
||||
|
||||
// Store the dot for redrawing
|
||||
this.lineSegments.push({
|
||||
type: 'dot',
|
||||
x: x,
|
||||
y: y,
|
||||
color: this.brushColor,
|
||||
size: this.brushSize
|
||||
});
|
||||
} else {
|
||||
// Connect to the previous point
|
||||
this.ctx.moveTo(this.lastX, this.lastY);
|
||||
this.ctx.lineTo(x, y);
|
||||
|
||||
// Store the line segment for redrawing
|
||||
this.lineSegments.push({
|
||||
type: 'line',
|
||||
x1: this.lastX,
|
||||
y1: this.lastY,
|
||||
x2: x,
|
||||
y2: y,
|
||||
color: this.brushColor,
|
||||
size: this.brushSize
|
||||
});
|
||||
}
|
||||
|
||||
this.ctx.stroke();
|
||||
|
||||
this.lastX = x;
|
||||
this.lastY = y;
|
||||
}
|
||||
|
||||
handleCanvasClick(e) {
|
||||
if (this.currentTool === 'text' && this.isTextPlacementMode) {
|
||||
this.placeText(e);
|
||||
}
|
||||
}
|
||||
|
||||
onTouchStart(e) {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
|
||||
// If in text placement mode, handle as a click
|
||||
if (this.currentTool === 'text' && this.isTextPlacementMode) {
|
||||
const mouseEvent = new MouseEvent('click', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
});
|
||||
this.canvas.dispatchEvent(mouseEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise handle as normal drawing
|
||||
const mouseEvent = new MouseEvent('mousedown', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
});
|
||||
this.canvas.dispatchEvent(mouseEvent);
|
||||
}
|
||||
|
||||
onTouchMove(e) {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
const mouseEvent = new MouseEvent('mousemove', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
});
|
||||
this.canvas.dispatchEvent(mouseEvent);
|
||||
}
|
||||
|
||||
onTouchEnd(e) {
|
||||
e.preventDefault();
|
||||
this.endPaint();
|
||||
}
|
||||
|
||||
dragText(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / rect.width;
|
||||
const scaleY = this.canvas.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Update text position with offset
|
||||
this.selectedTextElement.x = x + this.dragOffsetX;
|
||||
this.selectedTextElement.y = y + this.dragOffsetY;
|
||||
|
||||
// Redraw selected text element
|
||||
if (this.draggingCanvasContext) {
|
||||
this.ctx.putImageData(this.draggingCanvasContext, 0, 0);
|
||||
} else {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
this.ctx.font = this.selectedTextElement.font;
|
||||
this.ctx.fillStyle = this.selectedTextElement.color;
|
||||
this.ctx.fillText(this.selectedTextElement.text, this.selectedTextElement.x, this.selectedTextElement.y);
|
||||
}
|
||||
|
||||
findTextElementAt(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / rect.width;
|
||||
const scaleY = this.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 = this.textElements.length - 1; i >= 0; i--) {
|
||||
const text = this.textElements[i];
|
||||
|
||||
// Calculate text dimensions
|
||||
this.ctx.font = text.font;
|
||||
const textWidth = this.ctx.measureText(text.text).width;
|
||||
|
||||
// Extract font size correctly from the font string
|
||||
const fontSizeMatch = text.font.match(/(\d+)px/);
|
||||
const fontSize = fontSizeMatch ? parseInt(fontSizeMatch[1]) : 14;
|
||||
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;
|
||||
}
|
||||
|
||||
startTextPlacement() {
|
||||
const text = document.getElementById('text-input').value.trim();
|
||||
if (!text) {
|
||||
alert('请输入文字内容');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isTextPlacementMode = true;
|
||||
|
||||
// Add visual feedback
|
||||
setCanvasTitle('点击画布放置文字');
|
||||
this.canvas.classList.add('text-placement-mode');
|
||||
}
|
||||
|
||||
cancelTextPlacement() {
|
||||
this.isTextPlacementMode = false;
|
||||
this.canvas.classList.remove('text-placement-mode');
|
||||
|
||||
// reset dragging state
|
||||
this.isDraggingText = false;
|
||||
this.dragOffsetX = 0;
|
||||
this.dragOffsetY = 0;
|
||||
this.selectedTextElement = null;
|
||||
this.draggingCanvasContext = null;
|
||||
}
|
||||
|
||||
placeText(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scaleX = this.canvas.width / rect.width;
|
||||
const scaleY = this.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 (this.textItalic) fontStyle += 'italic ';
|
||||
if (this.textBold) fontStyle += 'bold ';
|
||||
|
||||
// Create a new text element
|
||||
const newText = {
|
||||
text: text,
|
||||
x: x,
|
||||
y: y,
|
||||
font: `${fontStyle}${fontSize}px ${fontFamily}`,
|
||||
color: this.brushColor
|
||||
};
|
||||
|
||||
// Add to our list of text elements
|
||||
this.textElements.push(newText);
|
||||
|
||||
// Select this text element for immediate dragging
|
||||
this.selectedTextElement = newText;
|
||||
this.draggingCanvasContext = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Draw text on canvas
|
||||
this.ctx.font = newText.font;
|
||||
this.ctx.fillStyle = newText.color;
|
||||
this.ctx.fillText(newText.text, newText.x, newText.y);
|
||||
|
||||
// Save to history after placing text
|
||||
this.saveToHistory();
|
||||
|
||||
// Reset
|
||||
document.getElementById('text-input').value = '';
|
||||
this.isTextPlacementMode = false;
|
||||
this.canvas.classList.remove('text-placement-mode');
|
||||
setCanvasTitle('拖动新添加文字可调整位置');
|
||||
}
|
||||
|
||||
redrawTextElements() {
|
||||
// Redraw all text elements after dithering
|
||||
this.textElements.forEach(item => {
|
||||
this.ctx.font = item.font;
|
||||
this.ctx.fillStyle = item.color;
|
||||
this.ctx.fillText(item.text, item.x, item.y);
|
||||
});
|
||||
}
|
||||
|
||||
redrawLineSegments() {
|
||||
// Redraw all line segments after dithering
|
||||
this.lineSegments.forEach(segment => {
|
||||
this.ctx.lineJoin = 'round';
|
||||
this.ctx.lineCap = 'round';
|
||||
this.ctx.strokeStyle = segment.color;
|
||||
this.ctx.lineWidth = segment.size;
|
||||
this.ctx.beginPath();
|
||||
|
||||
if (segment.type === 'dot') {
|
||||
this.ctx.moveTo(segment.x, segment.y);
|
||||
this.ctx.lineTo(segment.x + 0.1, segment.y + 0.1);
|
||||
} else {
|
||||
this.ctx.moveTo(segment.x1, segment.y1);
|
||||
this.ctx.lineTo(segment.x2, segment.y2);
|
||||
}
|
||||
|
||||
this.ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
clearElements() {
|
||||
this.textElements = [];
|
||||
this.lineSegments = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user