mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 14:25:44 -03:00
add advanced brush cursor visualization
Implemented dynamic brush cursor with visual feedback for size (circle radius), strength (opacity), and hardness (solid/dashed border with gradient). Added overlay canvas system for smooth cursor updates without affecting main rendering performance.
This commit is contained in:
@@ -61,6 +61,15 @@ export class Canvas {
|
|||||||
});
|
});
|
||||||
this.offscreenCanvas = offscreenCanvas;
|
this.offscreenCanvas = offscreenCanvas;
|
||||||
this.offscreenCtx = offscreenCtx;
|
this.offscreenCtx = offscreenCtx;
|
||||||
|
// Create overlay canvas for brush cursor and other lightweight overlays
|
||||||
|
const { canvas: overlayCanvas, ctx: overlayCtx } = createCanvas(0, 0, '2d', {
|
||||||
|
alpha: true,
|
||||||
|
willReadFrequently: false
|
||||||
|
});
|
||||||
|
if (!overlayCtx)
|
||||||
|
throw new Error("Could not create overlay canvas context");
|
||||||
|
this.overlayCanvas = overlayCanvas;
|
||||||
|
this.overlayCtx = overlayCtx;
|
||||||
this.canvasContainer = null;
|
this.canvasContainer = null;
|
||||||
this.dataInitialized = false;
|
this.dataInitialized = false;
|
||||||
this.pendingDataCheck = null;
|
this.pendingDataCheck = null;
|
||||||
|
|||||||
@@ -234,7 +234,10 @@ export class CanvasInteractions {
|
|||||||
switch (this.interaction.mode) {
|
switch (this.interaction.mode) {
|
||||||
case 'drawingMask':
|
case 'drawingMask':
|
||||||
this.canvas.maskTool.handleMouseMove(coords.world, coords.view);
|
this.canvas.maskTool.handleMouseMove(coords.world, coords.view);
|
||||||
this.canvas.render();
|
// Only render if actually drawing, not just moving cursor
|
||||||
|
if (this.canvas.maskTool.isDrawing) {
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'panning':
|
case 'panning':
|
||||||
this.panViewport(e);
|
this.panViewport(e);
|
||||||
@@ -256,6 +259,10 @@ export class CanvasInteractions {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.updateCursor(coords.world);
|
this.updateCursor(coords.world);
|
||||||
|
// Update brush cursor on overlay if mask tool is active
|
||||||
|
if (this.canvas.maskTool.isActive) {
|
||||||
|
this.canvas.canvasRenderer.drawMaskBrushCursor(coords.world);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// --- DYNAMICZNY PODGLĄD LINII CUSTOM SHAPE ---
|
// --- DYNAMICZNY PODGLĄD LINII CUSTOM SHAPE ---
|
||||||
@@ -350,8 +357,17 @@ export class CanvasInteractions {
|
|||||||
this.performZoomOperation(coords.world, zoomFactor);
|
this.performZoomOperation(coords.world, zoomFactor);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Layer transformation when layers are selected
|
// Check if mouse is over any selected layer
|
||||||
this.handleLayerWheelTransformation(e);
|
const isOverSelectedLayer = this.isPointInSelectedLayers(coords.world.x, coords.world.y);
|
||||||
|
if (isOverSelectedLayer) {
|
||||||
|
// Layer transformation when layers are selected and mouse is over selected layer
|
||||||
|
this.handleLayerWheelTransformation(e);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Zoom operation when mouse is not over selected layers
|
||||||
|
const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
|
||||||
|
this.performZoomOperation(coords.world, zoomFactor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
if (!this.canvas.maskTool.isActive) {
|
if (!this.canvas.maskTool.isActive) {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export class CanvasRenderer {
|
|||||||
this.lastRenderTime = 0;
|
this.lastRenderTime = 0;
|
||||||
this.renderInterval = 1000 / 60;
|
this.renderInterval = 1000 / 60;
|
||||||
this.isDirty = false;
|
this.isDirty = false;
|
||||||
|
// Initialize overlay canvas
|
||||||
|
this.initOverlay();
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Helper function to draw text with background at world coordinates
|
* Helper function to draw text with background at world coordinates
|
||||||
@@ -158,6 +160,9 @@ export class CanvasRenderer {
|
|||||||
this.canvas.canvas.height = this.canvas.offscreenCanvas.height;
|
this.canvas.canvas.height = this.canvas.offscreenCanvas.height;
|
||||||
}
|
}
|
||||||
this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0);
|
this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0);
|
||||||
|
// Ensure overlay canvas is in DOM and properly sized
|
||||||
|
this.addOverlayToDOM();
|
||||||
|
this.updateOverlaySize();
|
||||||
// Update Batch Preview UI positions
|
// Update Batch Preview UI positions
|
||||||
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
|
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
|
||||||
this.canvas.batchPreviewManagers.forEach((manager) => {
|
this.canvas.batchPreviewManagers.forEach((manager) => {
|
||||||
@@ -583,4 +588,125 @@ export class CanvasRenderer {
|
|||||||
padding: 8
|
padding: 8
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Initialize overlay canvas for lightweight overlays like brush cursor
|
||||||
|
*/
|
||||||
|
initOverlay() {
|
||||||
|
// Setup overlay canvas to match main canvas
|
||||||
|
this.updateOverlaySize();
|
||||||
|
// Position overlay canvas on top of main canvas
|
||||||
|
this.canvas.overlayCanvas.style.position = 'absolute';
|
||||||
|
this.canvas.overlayCanvas.style.left = '0px';
|
||||||
|
this.canvas.overlayCanvas.style.top = '0px';
|
||||||
|
this.canvas.overlayCanvas.style.pointerEvents = 'none';
|
||||||
|
this.canvas.overlayCanvas.style.zIndex = '20'; // Above other overlays
|
||||||
|
// Add overlay to DOM when main canvas is added
|
||||||
|
this.addOverlayToDOM();
|
||||||
|
log.debug('Overlay canvas initialized');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add overlay canvas to DOM if main canvas has a parent
|
||||||
|
*/
|
||||||
|
addOverlayToDOM() {
|
||||||
|
if (this.canvas.canvas.parentElement && !this.canvas.overlayCanvas.parentElement) {
|
||||||
|
this.canvas.canvas.parentElement.appendChild(this.canvas.overlayCanvas);
|
||||||
|
log.debug('Overlay canvas added to DOM');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Update overlay canvas size to match main canvas
|
||||||
|
*/
|
||||||
|
updateOverlaySize() {
|
||||||
|
if (this.canvas.overlayCanvas.width !== this.canvas.canvas.clientWidth ||
|
||||||
|
this.canvas.overlayCanvas.height !== this.canvas.canvas.clientHeight) {
|
||||||
|
this.canvas.overlayCanvas.width = Math.max(1, this.canvas.canvas.clientWidth);
|
||||||
|
this.canvas.overlayCanvas.height = Math.max(1, this.canvas.canvas.clientHeight);
|
||||||
|
log.debug(`Overlay canvas resized to ${this.canvas.overlayCanvas.width}x${this.canvas.overlayCanvas.height}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Clear overlay canvas
|
||||||
|
*/
|
||||||
|
clearOverlay() {
|
||||||
|
this.canvas.overlayCtx.clearRect(0, 0, this.canvas.overlayCanvas.width, this.canvas.overlayCanvas.height);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Draw mask brush cursor on overlay canvas with visual feedback for size, strength and hardness
|
||||||
|
* @param worldPoint World coordinates of cursor
|
||||||
|
*/
|
||||||
|
drawMaskBrushCursor(worldPoint) {
|
||||||
|
if (!this.canvas.maskTool.isActive || !this.canvas.isMouseOver) {
|
||||||
|
this.clearOverlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Update overlay size if needed
|
||||||
|
this.updateOverlaySize();
|
||||||
|
// Clear previous cursor
|
||||||
|
this.clearOverlay();
|
||||||
|
// Convert world coordinates to screen coordinates
|
||||||
|
const screenX = (worldPoint.x - this.canvas.viewport.x) * this.canvas.viewport.zoom;
|
||||||
|
const screenY = (worldPoint.y - this.canvas.viewport.y) * this.canvas.viewport.zoom;
|
||||||
|
// Get brush properties
|
||||||
|
const brushRadius = (this.canvas.maskTool.brushSize / 2) * this.canvas.viewport.zoom;
|
||||||
|
const brushStrength = this.canvas.maskTool.brushStrength;
|
||||||
|
const brushHardness = this.canvas.maskTool.brushHardness;
|
||||||
|
// Save context state
|
||||||
|
this.canvas.overlayCtx.save();
|
||||||
|
// 1. Draw inner fill to visualize STRENGTH (opacity)
|
||||||
|
// Higher strength = more opaque fill
|
||||||
|
this.canvas.overlayCtx.beginPath();
|
||||||
|
this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI);
|
||||||
|
this.canvas.overlayCtx.fillStyle = `rgba(255, 255, 255, ${brushStrength * 0.2})`; // Max 20% opacity for visibility
|
||||||
|
this.canvas.overlayCtx.fill();
|
||||||
|
// 2. Draw gradient edge to visualize HARDNESS
|
||||||
|
// Hard brush = sharp edge, Soft brush = gradient edge
|
||||||
|
if (brushHardness < 1) {
|
||||||
|
// Create radial gradient for soft brushes
|
||||||
|
const innerRadius = brushRadius * brushHardness;
|
||||||
|
const gradient = this.canvas.overlayCtx.createRadialGradient(screenX, screenY, innerRadius, screenX, screenY, brushRadius);
|
||||||
|
// Inner part is solid
|
||||||
|
gradient.addColorStop(0, `rgba(255, 255, 255, ${0.3 + brushStrength * 0.3})`);
|
||||||
|
// Outer part fades based on hardness
|
||||||
|
gradient.addColorStop(1, `rgba(255, 255, 255, ${0.05})`);
|
||||||
|
this.canvas.overlayCtx.beginPath();
|
||||||
|
this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI);
|
||||||
|
this.canvas.overlayCtx.fillStyle = gradient;
|
||||||
|
this.canvas.overlayCtx.fill();
|
||||||
|
}
|
||||||
|
// 3. Draw outer circle (SIZE indicator)
|
||||||
|
this.canvas.overlayCtx.beginPath();
|
||||||
|
this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI);
|
||||||
|
// Make the stroke opacity also reflect strength slightly
|
||||||
|
const strokeOpacity = 0.4 + brushStrength * 0.4; // Range from 0.4 to 0.8
|
||||||
|
this.canvas.overlayCtx.strokeStyle = `rgba(255, 255, 255, ${strokeOpacity})`;
|
||||||
|
this.canvas.overlayCtx.lineWidth = 1.5;
|
||||||
|
// Use solid line for hard brushes, dashed for soft brushes
|
||||||
|
if (brushHardness > 0.8) {
|
||||||
|
// Hard brush - solid line
|
||||||
|
this.canvas.overlayCtx.setLineDash([]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Soft brush - dashed line, dash length based on hardness
|
||||||
|
const dashLength = 2 + (1 - brushHardness) * 4; // Longer dashes for softer brushes
|
||||||
|
this.canvas.overlayCtx.setLineDash([dashLength, dashLength]);
|
||||||
|
}
|
||||||
|
this.canvas.overlayCtx.stroke();
|
||||||
|
// 4. Optional: Draw center dot for very precise brushes
|
||||||
|
if (brushRadius < 5) {
|
||||||
|
this.canvas.overlayCtx.beginPath();
|
||||||
|
this.canvas.overlayCtx.arc(screenX, screenY, 1, 0, 2 * Math.PI);
|
||||||
|
this.canvas.overlayCtx.fillStyle = `rgba(255, 255, 255, ${strokeOpacity})`;
|
||||||
|
this.canvas.overlayCtx.fill();
|
||||||
|
}
|
||||||
|
// Restore context state
|
||||||
|
this.canvas.overlayCtx.restore();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Update overlay position when viewport changes
|
||||||
|
*/
|
||||||
|
updateOverlayPosition() {
|
||||||
|
// Overlay canvas is positioned absolutely, so it doesn't need repositioning
|
||||||
|
// Just ensure it's the right size
|
||||||
|
this.updateOverlaySize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ export class MaskTool {
|
|||||||
this.isOverlayVisible = true;
|
this.isOverlayVisible = true;
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
this.brushSize = 20;
|
this.brushSize = 20;
|
||||||
this.brushStrength = 0.5;
|
this._brushStrength = 0.5;
|
||||||
this.brushHardness = 0.5;
|
this._brushHardness = 0.5;
|
||||||
this.isDrawing = false;
|
this.isDrawing = false;
|
||||||
this.lastPosition = null;
|
this.lastPosition = null;
|
||||||
const { canvas: previewCanvas, ctx: previewCtx } = createCanvas(1, 1, '2d', { willReadFrequently: true });
|
const { canvas: previewCanvas, ctx: previewCtx } = createCanvas(1, 1, '2d', { willReadFrequently: true });
|
||||||
@@ -79,8 +79,15 @@ export class MaskTool {
|
|||||||
this.canvasInstance.canvas.parentElement.appendChild(this.previewCanvas);
|
this.canvasInstance.canvas.parentElement.appendChild(this.previewCanvas);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Getters for brush properties
|
||||||
|
get brushStrength() {
|
||||||
|
return this._brushStrength;
|
||||||
|
}
|
||||||
|
get brushHardness() {
|
||||||
|
return this._brushHardness;
|
||||||
|
}
|
||||||
setBrushHardness(hardness) {
|
setBrushHardness(hardness) {
|
||||||
this.brushHardness = Math.max(0, Math.min(1, hardness));
|
this._brushHardness = Math.max(0, Math.min(1, hardness));
|
||||||
}
|
}
|
||||||
initMaskCanvas() {
|
initMaskCanvas() {
|
||||||
// Initialize chunked system
|
// Initialize chunked system
|
||||||
@@ -671,7 +678,7 @@ export class MaskTool {
|
|||||||
this.brushSize = Math.max(1, size);
|
this.brushSize = Math.max(1, size);
|
||||||
}
|
}
|
||||||
setBrushStrength(strength) {
|
setBrushStrength(strength) {
|
||||||
this.brushStrength = Math.max(0, Math.min(1, strength));
|
this._brushStrength = Math.max(0, Math.min(1, strength));
|
||||||
}
|
}
|
||||||
handleMouseDown(worldCoords, viewCoords) {
|
handleMouseDown(worldCoords, viewCoords) {
|
||||||
if (!this.isActive)
|
if (!this.isActive)
|
||||||
@@ -697,6 +704,8 @@ export class MaskTool {
|
|||||||
handleMouseLeave() {
|
handleMouseLeave() {
|
||||||
this.previewVisible = false;
|
this.previewVisible = false;
|
||||||
this.clearPreview();
|
this.clearPreview();
|
||||||
|
// Clear overlay canvas when mouse leaves
|
||||||
|
this.canvasInstance.canvasRenderer.clearOverlay();
|
||||||
}
|
}
|
||||||
handleMouseEnter() {
|
handleMouseEnter() {
|
||||||
this.previewVisible = true;
|
this.previewVisible = true;
|
||||||
@@ -767,13 +776,13 @@ export class MaskTool {
|
|||||||
chunk.ctx.moveTo(startLocal.x, startLocal.y);
|
chunk.ctx.moveTo(startLocal.x, startLocal.y);
|
||||||
chunk.ctx.lineTo(endLocal.x, endLocal.y);
|
chunk.ctx.lineTo(endLocal.x, endLocal.y);
|
||||||
const gradientRadius = this.brushSize / 2;
|
const gradientRadius = this.brushSize / 2;
|
||||||
if (this.brushHardness === 1) {
|
if (this._brushHardness === 1) {
|
||||||
chunk.ctx.strokeStyle = `rgba(255, 255, 255, ${this.brushStrength})`;
|
chunk.ctx.strokeStyle = `rgba(255, 255, 255, ${this._brushStrength})`;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const innerRadius = gradientRadius * this.brushHardness;
|
const innerRadius = gradientRadius * this._brushHardness;
|
||||||
const gradient = chunk.ctx.createRadialGradient(endLocal.x, endLocal.y, innerRadius, endLocal.x, endLocal.y, gradientRadius);
|
const gradient = chunk.ctx.createRadialGradient(endLocal.x, endLocal.y, innerRadius, endLocal.x, endLocal.y, gradientRadius);
|
||||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${this.brushStrength})`);
|
gradient.addColorStop(0, `rgba(255, 255, 255, ${this._brushStrength})`);
|
||||||
gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
|
gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
|
||||||
chunk.ctx.strokeStyle = gradient;
|
chunk.ctx.strokeStyle = gradient;
|
||||||
}
|
}
|
||||||
@@ -903,18 +912,12 @@ export class MaskTool {
|
|||||||
}
|
}
|
||||||
drawBrushPreview(viewCoords) {
|
drawBrushPreview(viewCoords) {
|
||||||
if (!this.previewVisible || this.isDrawing) {
|
if (!this.previewVisible || this.isDrawing) {
|
||||||
this.clearPreview();
|
this.canvasInstance.canvasRenderer.clearOverlay();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.clearPreview();
|
// Use overlay canvas instead of preview canvas for brush cursor
|
||||||
const zoom = this.canvasInstance.viewport.zoom;
|
const worldCoords = this.canvasInstance.lastMousePosition;
|
||||||
const radius = (this.brushSize / 2) * zoom;
|
this.canvasInstance.canvasRenderer.drawMaskBrushCursor(worldCoords);
|
||||||
this.previewCtx.beginPath();
|
|
||||||
this.previewCtx.arc(viewCoords.x, viewCoords.y, radius, 0, 2 * Math.PI);
|
|
||||||
this.previewCtx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
|
|
||||||
this.previewCtx.lineWidth = 1;
|
|
||||||
this.previewCtx.setLineDash([2, 4]);
|
|
||||||
this.previewCtx.stroke();
|
|
||||||
}
|
}
|
||||||
clearPreview() {
|
clearPreview() {
|
||||||
this.previewCtx.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height);
|
this.previewCtx.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height);
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ export class Canvas {
|
|||||||
node: ComfyNode;
|
node: ComfyNode;
|
||||||
offscreenCanvas: HTMLCanvasElement;
|
offscreenCanvas: HTMLCanvasElement;
|
||||||
offscreenCtx: CanvasRenderingContext2D | null;
|
offscreenCtx: CanvasRenderingContext2D | null;
|
||||||
|
overlayCanvas: HTMLCanvasElement;
|
||||||
|
overlayCtx: CanvasRenderingContext2D;
|
||||||
onHistoryChange: ((historyInfo: { canUndo: boolean; canRedo: boolean; }) => void) | undefined;
|
onHistoryChange: ((historyInfo: { canUndo: boolean; canRedo: boolean; }) => void) | undefined;
|
||||||
onViewportChange: (() => void) | null;
|
onViewportChange: (() => void) | null;
|
||||||
onStateChange: (() => void) | undefined;
|
onStateChange: (() => void) | undefined;
|
||||||
@@ -122,6 +124,16 @@ export class Canvas {
|
|||||||
});
|
});
|
||||||
this.offscreenCanvas = offscreenCanvas;
|
this.offscreenCanvas = offscreenCanvas;
|
||||||
this.offscreenCtx = offscreenCtx;
|
this.offscreenCtx = offscreenCtx;
|
||||||
|
|
||||||
|
// Create overlay canvas for brush cursor and other lightweight overlays
|
||||||
|
const { canvas: overlayCanvas, ctx: overlayCtx } = createCanvas(0, 0, '2d', {
|
||||||
|
alpha: true,
|
||||||
|
willReadFrequently: false
|
||||||
|
});
|
||||||
|
if (!overlayCtx) throw new Error("Could not create overlay canvas context");
|
||||||
|
this.overlayCanvas = overlayCanvas;
|
||||||
|
this.overlayCtx = overlayCtx;
|
||||||
|
|
||||||
this.canvasContainer = null;
|
this.canvasContainer = null;
|
||||||
|
|
||||||
this.dataInitialized = false;
|
this.dataInitialized = false;
|
||||||
|
|||||||
@@ -327,7 +327,10 @@ export class CanvasInteractions {
|
|||||||
switch (this.interaction.mode) {
|
switch (this.interaction.mode) {
|
||||||
case 'drawingMask':
|
case 'drawingMask':
|
||||||
this.canvas.maskTool.handleMouseMove(coords.world, coords.view);
|
this.canvas.maskTool.handleMouseMove(coords.world, coords.view);
|
||||||
this.canvas.render();
|
// Only render if actually drawing, not just moving cursor
|
||||||
|
if (this.canvas.maskTool.isDrawing) {
|
||||||
|
this.canvas.render();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'panning':
|
case 'panning':
|
||||||
this.panViewport(e);
|
this.panViewport(e);
|
||||||
@@ -349,6 +352,10 @@ export class CanvasInteractions {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.updateCursor(coords.world);
|
this.updateCursor(coords.world);
|
||||||
|
// Update brush cursor on overlay if mask tool is active
|
||||||
|
if (this.canvas.maskTool.isActive) {
|
||||||
|
this.canvas.canvasRenderer.drawMaskBrushCursor(coords.world);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,8 +467,17 @@ export class CanvasInteractions {
|
|||||||
const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
|
const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
|
||||||
this.performZoomOperation(coords.world, zoomFactor);
|
this.performZoomOperation(coords.world, zoomFactor);
|
||||||
} else {
|
} else {
|
||||||
// Layer transformation when layers are selected
|
// Check if mouse is over any selected layer
|
||||||
this.handleLayerWheelTransformation(e);
|
const isOverSelectedLayer = this.isPointInSelectedLayers(coords.world.x, coords.world.y);
|
||||||
|
|
||||||
|
if (isOverSelectedLayer) {
|
||||||
|
// Layer transformation when layers are selected and mouse is over selected layer
|
||||||
|
this.handleLayerWheelTransformation(e);
|
||||||
|
} else {
|
||||||
|
// Zoom operation when mouse is not over selected layers
|
||||||
|
const zoomFactor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
|
||||||
|
this.performZoomOperation(coords.world, zoomFactor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canvas.render();
|
this.canvas.render();
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export class CanvasRenderer {
|
|||||||
this.lastRenderTime = 0;
|
this.lastRenderTime = 0;
|
||||||
this.renderInterval = 1000 / 60;
|
this.renderInterval = 1000 / 60;
|
||||||
this.isDirty = false;
|
this.isDirty = false;
|
||||||
|
|
||||||
|
// Initialize overlay canvas
|
||||||
|
this.initOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -205,6 +208,10 @@ export class CanvasRenderer {
|
|||||||
}
|
}
|
||||||
this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0);
|
this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0);
|
||||||
|
|
||||||
|
// Ensure overlay canvas is in DOM and properly sized
|
||||||
|
this.addOverlayToDOM();
|
||||||
|
this.updateOverlaySize();
|
||||||
|
|
||||||
// Update Batch Preview UI positions
|
// Update Batch Preview UI positions
|
||||||
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
|
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
|
||||||
this.canvas.batchPreviewManagers.forEach((manager: any) => {
|
this.canvas.batchPreviewManagers.forEach((manager: any) => {
|
||||||
@@ -710,4 +717,153 @@ export class CanvasRenderer {
|
|||||||
padding: 8
|
padding: 8
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize overlay canvas for lightweight overlays like brush cursor
|
||||||
|
*/
|
||||||
|
initOverlay(): void {
|
||||||
|
// Setup overlay canvas to match main canvas
|
||||||
|
this.updateOverlaySize();
|
||||||
|
|
||||||
|
// Position overlay canvas on top of main canvas
|
||||||
|
this.canvas.overlayCanvas.style.position = 'absolute';
|
||||||
|
this.canvas.overlayCanvas.style.left = '0px';
|
||||||
|
this.canvas.overlayCanvas.style.top = '0px';
|
||||||
|
this.canvas.overlayCanvas.style.pointerEvents = 'none';
|
||||||
|
this.canvas.overlayCanvas.style.zIndex = '20'; // Above other overlays
|
||||||
|
|
||||||
|
// Add overlay to DOM when main canvas is added
|
||||||
|
this.addOverlayToDOM();
|
||||||
|
|
||||||
|
log.debug('Overlay canvas initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add overlay canvas to DOM if main canvas has a parent
|
||||||
|
*/
|
||||||
|
addOverlayToDOM(): void {
|
||||||
|
if (this.canvas.canvas.parentElement && !this.canvas.overlayCanvas.parentElement) {
|
||||||
|
this.canvas.canvas.parentElement.appendChild(this.canvas.overlayCanvas);
|
||||||
|
log.debug('Overlay canvas added to DOM');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update overlay canvas size to match main canvas
|
||||||
|
*/
|
||||||
|
updateOverlaySize(): void {
|
||||||
|
if (this.canvas.overlayCanvas.width !== this.canvas.canvas.clientWidth ||
|
||||||
|
this.canvas.overlayCanvas.height !== this.canvas.canvas.clientHeight) {
|
||||||
|
|
||||||
|
this.canvas.overlayCanvas.width = Math.max(1, this.canvas.canvas.clientWidth);
|
||||||
|
this.canvas.overlayCanvas.height = Math.max(1, this.canvas.canvas.clientHeight);
|
||||||
|
|
||||||
|
log.debug(`Overlay canvas resized to ${this.canvas.overlayCanvas.width}x${this.canvas.overlayCanvas.height}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear overlay canvas
|
||||||
|
*/
|
||||||
|
clearOverlay(): void {
|
||||||
|
this.canvas.overlayCtx.clearRect(0, 0, this.canvas.overlayCanvas.width, this.canvas.overlayCanvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw mask brush cursor on overlay canvas with visual feedback for size, strength and hardness
|
||||||
|
* @param worldPoint World coordinates of cursor
|
||||||
|
*/
|
||||||
|
drawMaskBrushCursor(worldPoint: { x: number, y: number }): void {
|
||||||
|
if (!this.canvas.maskTool.isActive || !this.canvas.isMouseOver) {
|
||||||
|
this.clearOverlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update overlay size if needed
|
||||||
|
this.updateOverlaySize();
|
||||||
|
|
||||||
|
// Clear previous cursor
|
||||||
|
this.clearOverlay();
|
||||||
|
|
||||||
|
// Convert world coordinates to screen coordinates
|
||||||
|
const screenX = (worldPoint.x - this.canvas.viewport.x) * this.canvas.viewport.zoom;
|
||||||
|
const screenY = (worldPoint.y - this.canvas.viewport.y) * this.canvas.viewport.zoom;
|
||||||
|
|
||||||
|
// Get brush properties
|
||||||
|
const brushRadius = (this.canvas.maskTool.brushSize / 2) * this.canvas.viewport.zoom;
|
||||||
|
const brushStrength = this.canvas.maskTool.brushStrength;
|
||||||
|
const brushHardness = this.canvas.maskTool.brushHardness;
|
||||||
|
|
||||||
|
// Save context state
|
||||||
|
this.canvas.overlayCtx.save();
|
||||||
|
|
||||||
|
// 1. Draw inner fill to visualize STRENGTH (opacity)
|
||||||
|
// Higher strength = more opaque fill
|
||||||
|
this.canvas.overlayCtx.beginPath();
|
||||||
|
this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI);
|
||||||
|
this.canvas.overlayCtx.fillStyle = `rgba(255, 255, 255, ${brushStrength * 0.2})`; // Max 20% opacity for visibility
|
||||||
|
this.canvas.overlayCtx.fill();
|
||||||
|
|
||||||
|
// 2. Draw gradient edge to visualize HARDNESS
|
||||||
|
// Hard brush = sharp edge, Soft brush = gradient edge
|
||||||
|
if (brushHardness < 1) {
|
||||||
|
// Create radial gradient for soft brushes
|
||||||
|
const innerRadius = brushRadius * brushHardness;
|
||||||
|
const gradient = this.canvas.overlayCtx.createRadialGradient(
|
||||||
|
screenX, screenY, innerRadius,
|
||||||
|
screenX, screenY, brushRadius
|
||||||
|
);
|
||||||
|
|
||||||
|
// Inner part is solid
|
||||||
|
gradient.addColorStop(0, `rgba(255, 255, 255, ${0.3 + brushStrength * 0.3})`);
|
||||||
|
// Outer part fades based on hardness
|
||||||
|
gradient.addColorStop(1, `rgba(255, 255, 255, ${0.05})`);
|
||||||
|
|
||||||
|
this.canvas.overlayCtx.beginPath();
|
||||||
|
this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI);
|
||||||
|
this.canvas.overlayCtx.fillStyle = gradient;
|
||||||
|
this.canvas.overlayCtx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Draw outer circle (SIZE indicator)
|
||||||
|
this.canvas.overlayCtx.beginPath();
|
||||||
|
this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI);
|
||||||
|
|
||||||
|
// Make the stroke opacity also reflect strength slightly
|
||||||
|
const strokeOpacity = 0.4 + brushStrength * 0.4; // Range from 0.4 to 0.8
|
||||||
|
this.canvas.overlayCtx.strokeStyle = `rgba(255, 255, 255, ${strokeOpacity})`;
|
||||||
|
this.canvas.overlayCtx.lineWidth = 1.5;
|
||||||
|
|
||||||
|
// Use solid line for hard brushes, dashed for soft brushes
|
||||||
|
if (brushHardness > 0.8) {
|
||||||
|
// Hard brush - solid line
|
||||||
|
this.canvas.overlayCtx.setLineDash([]);
|
||||||
|
} else {
|
||||||
|
// Soft brush - dashed line, dash length based on hardness
|
||||||
|
const dashLength = 2 + (1 - brushHardness) * 4; // Longer dashes for softer brushes
|
||||||
|
this.canvas.overlayCtx.setLineDash([dashLength, dashLength]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canvas.overlayCtx.stroke();
|
||||||
|
|
||||||
|
// 4. Optional: Draw center dot for very precise brushes
|
||||||
|
if (brushRadius < 5) {
|
||||||
|
this.canvas.overlayCtx.beginPath();
|
||||||
|
this.canvas.overlayCtx.arc(screenX, screenY, 1, 0, 2 * Math.PI);
|
||||||
|
this.canvas.overlayCtx.fillStyle = `rgba(255, 255, 255, ${strokeOpacity})`;
|
||||||
|
this.canvas.overlayCtx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore context state
|
||||||
|
this.canvas.overlayCtx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update overlay position when viewport changes
|
||||||
|
*/
|
||||||
|
updateOverlayPosition(): void {
|
||||||
|
// Overlay canvas is positioned absolutely, so it doesn't need repositioning
|
||||||
|
// Just ensure it's the right size
|
||||||
|
this.updateOverlaySize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ interface MaskChunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MaskTool {
|
export class MaskTool {
|
||||||
private brushHardness: number;
|
private _brushHardness: number;
|
||||||
private brushSize: number;
|
public brushSize: number;
|
||||||
private brushStrength: number;
|
private _brushStrength: number;
|
||||||
private canvasInstance: Canvas & { canvasState: CanvasState, width: number, height: number };
|
private canvasInstance: Canvas & { canvasState: CanvasState, width: number, height: number };
|
||||||
public isActive: boolean;
|
public isActive: boolean;
|
||||||
public isDrawing: boolean;
|
public isDrawing: boolean;
|
||||||
@@ -96,8 +96,8 @@ export class MaskTool {
|
|||||||
this.isOverlayVisible = true;
|
this.isOverlayVisible = true;
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
this.brushSize = 20;
|
this.brushSize = 20;
|
||||||
this.brushStrength = 0.5;
|
this._brushStrength = 0.5;
|
||||||
this.brushHardness = 0.5;
|
this._brushHardness = 0.5;
|
||||||
this.isDrawing = false;
|
this.isDrawing = false;
|
||||||
this.lastPosition = null;
|
this.lastPosition = null;
|
||||||
|
|
||||||
@@ -156,8 +156,17 @@ export class MaskTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Getters for brush properties
|
||||||
|
get brushStrength(): number {
|
||||||
|
return this._brushStrength;
|
||||||
|
}
|
||||||
|
|
||||||
|
get brushHardness(): number {
|
||||||
|
return this._brushHardness;
|
||||||
|
}
|
||||||
|
|
||||||
setBrushHardness(hardness: number): void {
|
setBrushHardness(hardness: number): void {
|
||||||
this.brushHardness = Math.max(0, Math.min(1, hardness));
|
this._brushHardness = Math.max(0, Math.min(1, hardness));
|
||||||
}
|
}
|
||||||
|
|
||||||
initMaskCanvas(): void {
|
initMaskCanvas(): void {
|
||||||
@@ -867,7 +876,7 @@ export class MaskTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setBrushStrength(strength: number): void {
|
setBrushStrength(strength: number): void {
|
||||||
this.brushStrength = Math.max(0, Math.min(1, strength));
|
this._brushStrength = Math.max(0, Math.min(1, strength));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseDown(worldCoords: Point, viewCoords: Point): void {
|
handleMouseDown(worldCoords: Point, viewCoords: Point): void {
|
||||||
@@ -898,6 +907,8 @@ export class MaskTool {
|
|||||||
handleMouseLeave(): void {
|
handleMouseLeave(): void {
|
||||||
this.previewVisible = false;
|
this.previewVisible = false;
|
||||||
this.clearPreview();
|
this.clearPreview();
|
||||||
|
// Clear overlay canvas when mouse leaves
|
||||||
|
this.canvasInstance.canvasRenderer.clearOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseEnter(): void {
|
handleMouseEnter(): void {
|
||||||
@@ -982,15 +993,15 @@ export class MaskTool {
|
|||||||
|
|
||||||
const gradientRadius = this.brushSize / 2;
|
const gradientRadius = this.brushSize / 2;
|
||||||
|
|
||||||
if (this.brushHardness === 1) {
|
if (this._brushHardness === 1) {
|
||||||
chunk.ctx.strokeStyle = `rgba(255, 255, 255, ${this.brushStrength})`;
|
chunk.ctx.strokeStyle = `rgba(255, 255, 255, ${this._brushStrength})`;
|
||||||
} else {
|
} else {
|
||||||
const innerRadius = gradientRadius * this.brushHardness;
|
const innerRadius = gradientRadius * this._brushHardness;
|
||||||
const gradient = chunk.ctx.createRadialGradient(
|
const gradient = chunk.ctx.createRadialGradient(
|
||||||
endLocal.x, endLocal.y, innerRadius,
|
endLocal.x, endLocal.y, innerRadius,
|
||||||
endLocal.x, endLocal.y, gradientRadius
|
endLocal.x, endLocal.y, gradientRadius
|
||||||
);
|
);
|
||||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${this.brushStrength})`);
|
gradient.addColorStop(0, `rgba(255, 255, 255, ${this._brushStrength})`);
|
||||||
gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
|
gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
|
||||||
chunk.ctx.strokeStyle = gradient;
|
chunk.ctx.strokeStyle = gradient;
|
||||||
}
|
}
|
||||||
@@ -1142,20 +1153,13 @@ export class MaskTool {
|
|||||||
|
|
||||||
drawBrushPreview(viewCoords: Point): void {
|
drawBrushPreview(viewCoords: Point): void {
|
||||||
if (!this.previewVisible || this.isDrawing) {
|
if (!this.previewVisible || this.isDrawing) {
|
||||||
this.clearPreview();
|
this.canvasInstance.canvasRenderer.clearOverlay();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clearPreview();
|
// Use overlay canvas instead of preview canvas for brush cursor
|
||||||
const zoom = this.canvasInstance.viewport.zoom;
|
const worldCoords = this.canvasInstance.lastMousePosition;
|
||||||
const radius = (this.brushSize / 2) * zoom;
|
this.canvasInstance.canvasRenderer.drawMaskBrushCursor(worldCoords);
|
||||||
|
|
||||||
this.previewCtx.beginPath();
|
|
||||||
this.previewCtx.arc(viewCoords.x, viewCoords.y, radius, 0, 2 * Math.PI);
|
|
||||||
this.previewCtx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
|
|
||||||
this.previewCtx.lineWidth = 1;
|
|
||||||
this.previewCtx.setLineDash([2, 4]);
|
|
||||||
this.previewCtx.stroke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPreview(): void {
|
clearPreview(): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user