fix brush cursor visibility

This commit is contained in:
Shuanglei Tao
2025-11-09 11:14:01 +08:00
parent 3b81651fa5
commit d31803f064
4 changed files with 66 additions and 72 deletions

View File

@@ -22,7 +22,7 @@
</div> </div>
<div class="flex-group right debug"> <div class="flex-group right debug">
<label for="epddriver">驱动</label> <label for="epddriver">驱动</label>
<select id="epddriver"> <select id="epddriver" onchange="updateDitcherOptions()">
<option value="01" data-color="blackWhiteColor" data-size="4.2_400_300">4.2寸 (黑白, UC8176)</option> <option value="01" data-color="blackWhiteColor" data-size="4.2_400_300">4.2寸 (黑白, UC8176)</option>
<option value="03" data-color="threeColor" data-size="4.2_400_300">4.2寸 (三色, UC8176)</option> <option value="03" data-color="threeColor" data-size="4.2_400_300">4.2寸 (三色, UC8176)</option>
<option value="04" data-color="blackWhiteColor" data-size="4.2_400_300">4.2寸 (黑白, SSD1619)</option> <option value="04" data-color="blackWhiteColor" data-size="4.2_400_300">4.2寸 (黑白, SSD1619)</option>
@@ -62,12 +62,12 @@
<fieldset> <fieldset>
<legend>蓝牙传图</legend> <legend>蓝牙传图</legend>
<div class="flex-container"> <div class="flex-container">
<input type="file" id="imageFile" accept=".png,.jpg,.bmp,.webp,.jpeg"> <input type="file" id="imageFile" accept=".png,.jpg,.bmp,.webp,.jpeg" onchange="updateImage()">
</div> </div>
<div class="flex-container options"> <div class="flex-container options">
<div class="flex-group debug"> <div class="flex-group debug">
<label for="canvasSize">画布尺寸:</label> <label for="canvasSize">画布尺寸:</label>
<select id="canvasSize"> <select id="canvasSize" onchange="updateCanvasSize()">
<option value="1.54_152_152">1.54 (152x152)</option> <option value="1.54_152_152">1.54 (152x152)</option>
<option value="1.54_200_200">1.54 (200x200)</option> <option value="1.54_200_200">1.54 (200x200)</option>
<option value="2.13_212_104">2.13 (212x104)</option> <option value="2.13_212_104">2.13 (212x104)</option>
@@ -94,7 +94,7 @@
</div> </div>
<div class="flex-group debug"> <div class="flex-group debug">
<label for="ditherMode">颜色模式:</label> <label for="ditherMode">颜色模式:</label>
<select id="ditherMode"> <select id="ditherMode" onchange="applyDither()">
<option value="blackWhiteColor">双色(黑白)</option> <option value="blackWhiteColor">双色(黑白)</option>
<option value="threeColor">三色(黑白红)</option> <option value="threeColor">三色(黑白红)</option>
<option value="fourColor">四色(黑白红黄)</option> <option value="fourColor">四色(黑白红黄)</option>
@@ -103,7 +103,7 @@
</div> </div>
<div class="flex-group"> <div class="flex-group">
<label for="ditherAlg">抖动算法:</label> <label for="ditherAlg">抖动算法:</label>
<select id="ditherAlg"> <select id="ditherAlg" onchange="applyDither()">
<option value="floydSteinberg">Floyd-Steinberg</option> <option value="floydSteinberg">Floyd-Steinberg</option>
<option value="atkinson">Atkinson</option> <option value="atkinson">Atkinson</option>
<option value="bayer">Bayer</option> <option value="bayer">Bayer</option>
@@ -217,7 +217,7 @@
<button id="crop-move-up" title="上移"></button> <button id="crop-move-up" title="上移"></button>
<button id="crop-move-down" title="下移"></button> <button id="crop-move-down" title="下移"></button>
<button id="crop-move-right" title="右移"></button> <button id="crop-move-right" title="右移"></button>
<button class="primary" onclick="cropManager.finishCrop()">完成</button> <button class="primary" onclick="applyDither()">完成</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,7 @@
class CropManager { class CropManager {
constructor(canvas, ctx, paintManager) { constructor(canvas, ctx) {
this.canvas = canvas; this.canvas = canvas;
this.ctx = ctx; this.ctx = ctx;
this.paintManager = paintManager;
this.backgroundZoom = 1; this.backgroundZoom = 1;
this.backgroundPanX = 0; this.backgroundPanX = 0;
this.backgroundPanY = 0; this.backgroundPanY = 0;
@@ -84,7 +83,7 @@ class CropManager {
this.canvas.parentNode.classList.add('crop-mode'); this.canvas.parentNode.classList.add('crop-mode');
} }
finishCrop() { finishCrop(callback) {
const imageFile = document.getElementById('imageFile'); const imageFile = document.getElementById('imageFile');
if (imageFile.files.length == 0) return; if (imageFile.files.length == 0) return;
@@ -103,12 +102,8 @@ class CropManager {
fillCanvas('white'); fillCanvas('white');
this.ctx.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, this.canvas.width, this.canvas.height);
this.paintManager.redrawTextElements();
this.paintManager.redrawLineSegments();
convertDithering();
this.exitCropMode(); this.exitCropMode();
this.paintManager.saveToHistory(); // Save after finishing crop if (callback) callback();
}; };
image.src = URL.createObjectURL(imageFile.files[0]); image.src = URL.createObjectURL(imageFile.files[0]);
} }

View File

@@ -492,10 +492,7 @@ function updateImage() {
if (image.width / image.height == canvas.width / canvas.height) { if (image.width / image.height == canvas.width / canvas.height) {
if (cropManager.isCropMode()) cropManager.exitCropMode(); if (cropManager.isCropMode()) cropManager.exitCropMode();
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height); ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);
paintManager.redrawTextElements();
paintManager.redrawLineSegments();
convertDithering(); convertDithering();
paintManager.saveToHistory(); // Save after loading image
} else { } else {
alert(`图片宽高比例与画布不匹配,将进入裁剪模式。\n请放大图片后移动图片使其充满画布, 再点击"完成"按钮。`); alert(`图片宽高比例与画布不匹配,将进入裁剪模式。\n请放大图片后移动图片使其充满画布, 再点击"完成"按钮。`);
paintManager.setActiveTool(null, ''); paintManager.setActiveTool(null, '');
@@ -548,6 +545,9 @@ function clearCanvas() {
} }
function convertDithering() { function convertDithering() {
paintManager.redrawTextElements();
paintManager.redrawLineSegments();
const contrast = parseFloat(document.getElementById('ditherContrast').value); const contrast = parseFloat(document.getElementById('ditherContrast').value);
const currentImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const currentImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const imageData = new ImageData( const imageData = new ImageData(
@@ -564,22 +564,23 @@ function convertDithering() {
const processedData = processImageData(ditherImage(imageData, alg, strength, mode), mode); const processedData = processImageData(ditherImage(imageData, alg, strength, mode), mode);
const finalImageData = decodeProcessedData(processedData, canvas.width, canvas.height, mode); const finalImageData = decodeProcessedData(processedData, canvas.width, canvas.height, mode);
ctx.putImageData(finalImageData, 0, 0); ctx.putImageData(finalImageData, 0, 0);
paintManager.saveToHistory(); // Save dithered image to history
}
function applyDither() {
cropManager.finishCrop(() => convertDithering());
} }
function initEventHandlers() { function initEventHandlers() {
document.getElementById("epddriver").addEventListener("change", updateDitcherOptions); document.getElementById("ditherStrength").addEventListener("input", (e) => {
document.getElementById("imageFile").addEventListener("change", updateImage); document.getElementById("ditherStrengthValue").innerText = parseFloat(e.target.value).toFixed(1);
document.getElementById("ditherMode").addEventListener("change", () => cropManager.finishCrop()); applyDither();
document.getElementById("ditherAlg").addEventListener("change", () => cropManager.finishCrop());
document.getElementById("ditherStrength").addEventListener("input", function () {
cropManager.finishCrop();
document.getElementById("ditherStrengthValue").innerText = parseFloat(this.value).toFixed(1);
}); });
document.getElementById("ditherContrast").addEventListener("input", function () { document.getElementById("ditherContrast").addEventListener("input", (e) => {
cropManager.finishCrop(); document.getElementById("ditherContrastValue").innerText = parseFloat(e.target.value).toFixed(1);
document.getElementById("ditherContrastValue").innerText = parseFloat(this.value).toFixed(1); applyDither();
}); });
document.getElementById("canvasSize").addEventListener("change", updateCanvasSize);
} }
function checkDebugMode() { function checkDebugMode() {

View File

@@ -174,9 +174,8 @@ class PaintManager {
document.addEventListener('keydown', this.handleKeyboard); document.addEventListener('keydown', this.handleKeyboard);
// Mouse move for brush cursor // Mouse move for brush cursor
this.canvas.addEventListener('mousemove', this.updateBrushCursor);
this.canvas.addEventListener('mouseenter', this.updateBrushCursor); this.canvas.addEventListener('mouseenter', this.updateBrushCursor);
this.canvas.addEventListener('mouseleave', this.hideBrushCursor); this.canvas.addEventListener('mousemove', this.updateBrushCursor);
// Create brush cursor element // Create brush cursor element
this.createBrushCursor(); this.createBrushCursor();
@@ -218,9 +217,6 @@ class PaintManager {
// Cancel any pending text placement // Cancel any pending text placement
this.cancelTextPlacement(); this.cancelTextPlacement();
// Update brush cursor visibility
this.updateBrushCursorVisibility();
} }
createBrushCursor() { createBrushCursor() {
@@ -259,54 +255,54 @@ class PaintManager {
this.brushCursor.style.height = size + 'px'; this.brushCursor.style.height = size + 'px';
} }
updateBrushCursorVisibility() {
if (!this.brushCursor) return;
if (this.currentTool === 'brush' || this.currentTool === 'eraser') {
this.brushCursor.style.display = 'block';
this.canvas.style.cursor = 'none';
} else {
this.brushCursor.style.display = 'none';
this.canvas.style.cursor = 'default';
}
}
updateBrushCursor(e) { updateBrushCursor(e) {
if (!this.brushCursor) return; if (!this.brushCursor) return;
if (this.currentTool === 'brush' || this.currentTool === 'eraser') { if (this.currentTool === 'brush' || this.currentTool === 'eraser') {
this.brushCursor.style.display = 'block'; // Check if mouse is within canvas bounds
this.canvas.style.cursor = 'none'; const rect = this.canvas.getBoundingClientRect();
const isInCanvas = e.clientX >= rect.left &&
e.clientX <= rect.right &&
e.clientY >= rect.top &&
e.clientY <= rect.bottom;
// Store the pending position if (isInCanvas) {
this.pendingCursorX = e.clientX; this.brushCursor.style.display = 'block';
this.pendingCursorY = e.clientY; this.canvas.style.cursor = 'none';
// Schedule update using requestAnimationFrame for smooth movement // Store the pending position
if (!this.cursorUpdateScheduled) { this.pendingCursorX = e.clientX;
this.cursorUpdateScheduled = true; this.pendingCursorY = e.clientY;
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) // Schedule update using requestAnimationFrame for smooth movement
if (this.currentTool === 'eraser') { if (!this.cursorUpdateScheduled) {
if (this.brushCursor.getAttribute('data-tool') !== 'eraser') { this.cursorUpdateScheduled = true;
this.brushCursor.style.border = '2px solid rgba(255, 0, 0, 0.7)'; requestAnimationFrame(() => {
this.brushCursor.style.backgroundColor = 'rgba(255, 255, 255, 0.2)'; this.brushCursor.style.transform = `translate(${this.pendingCursorX}px, ${this.pendingCursorY}px) translate(-50%, -50%)`;
this.brushCursor.style.boxShadow = 'none'; this.cursorUpdateScheduled = false;
this.brushCursor.setAttribute('data-tool', 'eraser'); });
}
// 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 { } else {
if (this.brushCursor.getAttribute('data-tool') !== 'brush') { // Hide cursor when outside canvas
// Use a contrasting border - white with black outline for visibility this.brushCursor.style.display = 'none';
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');
}
} }
} }
} }
@@ -353,6 +349,8 @@ class PaintManager {
this.isDraggingText = false; this.isDraggingText = false;
this.lastX = 0; this.lastX = 0;
this.lastY = 0; this.lastY = 0;
this.hideBrushCursor();
} }
paint(e) { paint(e) {