mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Refactor rendering logic to CanvasRenderer class
Moved all rendering-related methods from Canvas.js to a new CanvasRenderer.js module. Canvas now delegates rendering to CanvasRenderer, improving separation of concerns and maintainability.
This commit is contained in:
292
js/Canvas.js
292
js/Canvas.js
@@ -3,6 +3,7 @@ import {MaskTool} from "./Mask_tool.js";
|
||||
import {CanvasState} from "./CanvasState.js";
|
||||
import {CanvasInteractions} from "./CanvasInteractions.js";
|
||||
import {CanvasLayers} from "./CanvasLayers.js";
|
||||
import {CanvasRenderer} from "./CanvasRenderer.js";
|
||||
import {logger, LogLevel} from "./logger.js";
|
||||
|
||||
// Inicjalizacja loggera dla modułu Canvas
|
||||
@@ -55,6 +56,7 @@ export class Canvas {
|
||||
this.canvasState = new CanvasState(this); // Nowy moduł zarządzania stanem
|
||||
this.canvasInteractions = new CanvasInteractions(this); // Nowy moduł obsługi interakcji
|
||||
this.canvasLayers = new CanvasLayers(this); // Nowy moduł operacji na warstwach
|
||||
this.canvasRenderer = new CanvasRenderer(this); // Nowy moduł renderowania
|
||||
|
||||
// Po utworzeniu CanvasInteractions, użyj jego interaction state
|
||||
this.interaction = this.canvasInteractions.interaction;
|
||||
@@ -294,296 +296,10 @@ export class Canvas {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.renderAnimationFrame) {
|
||||
this.isDirty = true;
|
||||
return;
|
||||
}
|
||||
this.renderAnimationFrame = requestAnimationFrame(() => {
|
||||
const now = performance.now();
|
||||
if (now - this.lastRenderTime >= this.renderInterval) {
|
||||
this.lastRenderTime = now;
|
||||
this.actualRender();
|
||||
this.isDirty = false;
|
||||
}
|
||||
|
||||
if (this.isDirty) {
|
||||
this.renderAnimationFrame = null;
|
||||
this.render();
|
||||
} else {
|
||||
this.renderAnimationFrame = null;
|
||||
}
|
||||
});
|
||||
this.canvasRenderer.render();
|
||||
}
|
||||
|
||||
actualRender() {
|
||||
if (this.offscreenCanvas.width !== this.canvas.clientWidth ||
|
||||
this.offscreenCanvas.height !== this.canvas.clientHeight) {
|
||||
const newWidth = Math.max(1, this.canvas.clientWidth);
|
||||
const newHeight = Math.max(1, this.canvas.clientHeight);
|
||||
this.offscreenCanvas.width = newWidth;
|
||||
this.offscreenCanvas.height = newHeight;
|
||||
}
|
||||
|
||||
const ctx = this.offscreenCtx;
|
||||
|
||||
ctx.fillStyle = '#606060';
|
||||
ctx.fillRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height);
|
||||
|
||||
ctx.save();
|
||||
ctx.scale(this.viewport.zoom, this.viewport.zoom);
|
||||
ctx.translate(-this.viewport.x, -this.viewport.y);
|
||||
|
||||
this.drawGrid(ctx);
|
||||
|
||||
const sortedLayers = [...this.layers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
sortedLayers.forEach(layer => {
|
||||
if (!layer.image) return;
|
||||
ctx.save();
|
||||
const currentTransform = ctx.getTransform();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.globalCompositeOperation = layer.blendMode || 'normal';
|
||||
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||
ctx.setTransform(currentTransform);
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
ctx.translate(centerX, centerY);
|
||||
ctx.rotate(layer.rotation * Math.PI / 180);
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(
|
||||
layer.image, -layer.width / 2, -layer.height / 2,
|
||||
layer.width,
|
||||
layer.height
|
||||
);
|
||||
if (layer.mask) {
|
||||
}
|
||||
if (this.selectedLayers.includes(layer)) {
|
||||
this.drawSelectionFrame(ctx, layer);
|
||||
}
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
this.drawCanvasOutline(ctx);
|
||||
|
||||
// Renderowanie maski w zależności od trybu
|
||||
const maskImage = this.maskTool.getMask();
|
||||
if (this.maskTool.isActive) {
|
||||
// W trybie maski pokazuj maskę z przezroczystością 0.5
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.drawImage(maskImage, 0, 0);
|
||||
ctx.globalAlpha = 1.0;
|
||||
} else if (maskImage) {
|
||||
// W trybie warstw pokazuj maskę jako widoczną, ale nieedytowalną
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.globalAlpha = 1.0;
|
||||
ctx.drawImage(maskImage, 0, 0);
|
||||
ctx.globalAlpha = 1.0;
|
||||
}
|
||||
|
||||
if (this.interaction.mode === 'resizingCanvas' && this.interaction.canvasResizeRect) {
|
||||
const rect = this.interaction.canvasResizeRect;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)';
|
||||
ctx.lineWidth = 2 / this.viewport.zoom;
|
||||
ctx.setLineDash([8 / this.viewport.zoom, 4 / this.viewport.zoom]);
|
||||
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
||||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
||||
const textWorldX = rect.x + rect.width / 2;
|
||||
const textWorldY = rect.y + rect.height + (20 / this.viewport.zoom);
|
||||
|
||||
ctx.save();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
const screenX = (textWorldX - this.viewport.x) * this.viewport.zoom;
|
||||
const screenY = (textWorldY - this.viewport.y) * this.viewport.zoom;
|
||||
ctx.font = "14px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const bgWidth = textMetrics.width + 10;
|
||||
const bgHeight = 22;
|
||||
ctx.fillStyle = "rgba(0, 128, 0, 0.7)";
|
||||
ctx.fillRect(screenX - bgWidth / 2, screenY - bgHeight / 2, bgWidth, bgHeight);
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText(text, screenX, screenY);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
if (this.interaction.mode === 'movingCanvas' && this.interaction.canvasMoveRect) {
|
||||
const rect = this.interaction.canvasMoveRect;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)';
|
||||
ctx.lineWidth = 2 / this.viewport.zoom;
|
||||
ctx.setLineDash([10 / this.viewport.zoom, 5 / this.viewport.zoom]);
|
||||
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
||||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
|
||||
const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
|
||||
const textWorldX = rect.x + rect.width / 2;
|
||||
const textWorldY = rect.y - (20 / this.viewport.zoom);
|
||||
|
||||
ctx.save();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
const screenX = (textWorldX - this.viewport.x) * this.viewport.zoom;
|
||||
const screenY = (textWorldY - this.viewport.y) * this.viewport.zoom;
|
||||
ctx.font = "14px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const bgWidth = textMetrics.width + 10;
|
||||
const bgHeight = 22;
|
||||
ctx.fillStyle = "rgba(0, 100, 170, 0.7)";
|
||||
ctx.fillRect(screenX - bgWidth / 2, screenY - bgHeight / 2, bgWidth, bgHeight);
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText(text, screenX, screenY);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
if (this.selectedLayer) {
|
||||
this.selectedLayers.forEach(layer => {
|
||||
if (!layer.image) return;
|
||||
|
||||
const layerIndex = this.layers.indexOf(layer);
|
||||
const currentWidth = Math.round(layer.width);
|
||||
const currentHeight = Math.round(layer.height);
|
||||
const rotation = Math.round(layer.rotation % 360);
|
||||
const text = `${currentWidth}x${currentHeight} | ${rotation}° | Layer #${layerIndex + 1}`;
|
||||
|
||||
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
const rad = layer.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(rad);
|
||||
const sin = Math.sin(rad);
|
||||
|
||||
const halfW = layer.width / 2;
|
||||
const halfH = layer.height / 2;
|
||||
|
||||
const localCorners = [
|
||||
{x: -halfW, y: -halfH},
|
||||
{x: halfW, y: -halfH},
|
||||
{x: halfW, y: halfH},
|
||||
{x: -halfW, y: halfH}
|
||||
];
|
||||
const worldCorners = localCorners.map(p => ({
|
||||
x: centerX + p.x * cos - p.y * sin,
|
||||
y: centerY + p.x * sin + p.y * cos
|
||||
}));
|
||||
let minX = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
worldCorners.forEach(p => {
|
||||
minX = Math.min(minX, p.x);
|
||||
maxX = Math.max(maxX, p.x);
|
||||
maxY = Math.max(maxY, p.y);
|
||||
});
|
||||
const padding = 20 / this.viewport.zoom;
|
||||
const textWorldX = (minX + maxX) / 2;
|
||||
const textWorldY = maxY + padding;
|
||||
ctx.save();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
|
||||
const screenX = (textWorldX - this.viewport.x) * this.viewport.zoom;
|
||||
const screenY = (textWorldY - this.viewport.y) * this.viewport.zoom;
|
||||
|
||||
ctx.font = "14px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const textBgWidth = textMetrics.width + 10;
|
||||
const textBgHeight = 22;
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
|
||||
ctx.fillRect(screenX - textBgWidth / 2, screenY - textBgHeight / 2, textBgWidth, textBgHeight);
|
||||
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText(text, screenX, screenY);
|
||||
|
||||
ctx.restore();
|
||||
});
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
|
||||
if (this.canvas.width !== this.offscreenCanvas.width || this.canvas.height !== this.offscreenCanvas.height) {
|
||||
this.canvas.width = this.offscreenCanvas.width;
|
||||
this.canvas.height = this.offscreenCanvas.height;
|
||||
}
|
||||
this.ctx.drawImage(this.offscreenCanvas, 0, 0);
|
||||
}
|
||||
|
||||
drawGrid(ctx) {
|
||||
const gridSize = 64;
|
||||
const lineWidth = 0.5 / this.viewport.zoom;
|
||||
|
||||
const viewLeft = this.viewport.x;
|
||||
const viewTop = this.viewport.y;
|
||||
const viewRight = this.viewport.x + this.offscreenCanvas.width / this.viewport.zoom;
|
||||
const viewBottom = this.viewport.y + this.offscreenCanvas.height / this.viewport.zoom;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#707070';
|
||||
ctx.lineWidth = lineWidth;
|
||||
|
||||
for (let x = Math.floor(viewLeft / gridSize) * gridSize; x < viewRight; x += gridSize) {
|
||||
ctx.moveTo(x, viewTop);
|
||||
ctx.lineTo(x, viewBottom);
|
||||
}
|
||||
|
||||
for (let y = Math.floor(viewTop / gridSize) * gridSize; y < viewBottom; y += gridSize) {
|
||||
ctx.moveTo(viewLeft, y);
|
||||
ctx.lineTo(viewRight, y);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
drawCanvasOutline(ctx) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
ctx.lineWidth = 2 / this.viewport.zoom;
|
||||
ctx.setLineDash([10 / this.viewport.zoom, 5 / this.viewport.zoom]);
|
||||
|
||||
|
||||
ctx.rect(0, 0, this.width, this.height);
|
||||
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
drawSelectionFrame(ctx, layer) {
|
||||
const lineWidth = 2 / this.viewport.zoom;
|
||||
const handleRadius = 5 / this.viewport.zoom;
|
||||
ctx.strokeStyle = '#00ff00';
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.beginPath();
|
||||
ctx.rect(-layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -layer.height / 2);
|
||||
ctx.lineTo(0, -layer.height / 2 - 20 / this.viewport.zoom);
|
||||
ctx.stroke();
|
||||
const handles = this.getHandles(layer);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.lineWidth = 1 / this.viewport.zoom;
|
||||
|
||||
for (const key in handles) {
|
||||
const point = handles[key];
|
||||
ctx.beginPath();
|
||||
const localX = point.x - (layer.x + layer.width / 2);
|
||||
const localY = point.y - (layer.y + layer.height / 2);
|
||||
|
||||
const rad = -layer.rotation * Math.PI / 180;
|
||||
const rotatedX = localX * Math.cos(rad) - localY * Math.sin(rad);
|
||||
const rotatedY = localX * Math.sin(rad) + localY * Math.cos(rad);
|
||||
|
||||
ctx.arc(rotatedX, rotatedY, handleRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
// Rendering methods moved to CanvasRenderer
|
||||
|
||||
|
||||
getHandles(layer) {
|
||||
|
||||
323
js/CanvasRenderer.js
Normal file
323
js/CanvasRenderer.js
Normal file
@@ -0,0 +1,323 @@
|
||||
import {logger, LogLevel} from "./logger.js";
|
||||
|
||||
// Inicjalizacja loggera dla modułu CanvasRenderer
|
||||
const log = {
|
||||
debug: (...args) => logger.debug('CanvasRenderer', ...args),
|
||||
info: (...args) => logger.info('CanvasRenderer', ...args),
|
||||
warn: (...args) => logger.warn('CanvasRenderer', ...args),
|
||||
error: (...args) => logger.error('CanvasRenderer', ...args)
|
||||
};
|
||||
|
||||
// Konfiguracja loggera dla modułu CanvasRenderer
|
||||
logger.setModuleLevel('CanvasRenderer', LogLevel.DEBUG);
|
||||
|
||||
export class CanvasRenderer {
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.renderAnimationFrame = null;
|
||||
this.lastRenderTime = 0;
|
||||
this.renderInterval = 1000 / 60;
|
||||
this.isDirty = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.renderAnimationFrame) {
|
||||
this.isDirty = true;
|
||||
return;
|
||||
}
|
||||
this.renderAnimationFrame = requestAnimationFrame(() => {
|
||||
const now = performance.now();
|
||||
if (now - this.lastRenderTime >= this.renderInterval) {
|
||||
this.lastRenderTime = now;
|
||||
this.actualRender();
|
||||
this.isDirty = false;
|
||||
}
|
||||
|
||||
if (this.isDirty) {
|
||||
this.renderAnimationFrame = null;
|
||||
this.render();
|
||||
} else {
|
||||
this.renderAnimationFrame = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
actualRender() {
|
||||
if (this.canvas.offscreenCanvas.width !== this.canvas.canvas.clientWidth ||
|
||||
this.canvas.offscreenCanvas.height !== this.canvas.canvas.clientHeight) {
|
||||
const newWidth = Math.max(1, this.canvas.canvas.clientWidth);
|
||||
const newHeight = Math.max(1, this.canvas.canvas.clientHeight);
|
||||
this.canvas.offscreenCanvas.width = newWidth;
|
||||
this.canvas.offscreenCanvas.height = newHeight;
|
||||
}
|
||||
|
||||
const ctx = this.canvas.offscreenCtx;
|
||||
|
||||
ctx.fillStyle = '#606060';
|
||||
ctx.fillRect(0, 0, this.canvas.offscreenCanvas.width, this.canvas.offscreenCanvas.height);
|
||||
|
||||
ctx.save();
|
||||
ctx.scale(this.canvas.viewport.zoom, this.canvas.viewport.zoom);
|
||||
ctx.translate(-this.canvas.viewport.x, -this.canvas.viewport.y);
|
||||
|
||||
this.drawGrid(ctx);
|
||||
|
||||
const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
|
||||
sortedLayers.forEach(layer => {
|
||||
if (!layer.image) return;
|
||||
ctx.save();
|
||||
const currentTransform = ctx.getTransform();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.globalCompositeOperation = layer.blendMode || 'normal';
|
||||
ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
|
||||
ctx.setTransform(currentTransform);
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
ctx.translate(centerX, centerY);
|
||||
ctx.rotate(layer.rotation * Math.PI / 180);
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(
|
||||
layer.image, -layer.width / 2, -layer.height / 2,
|
||||
layer.width,
|
||||
layer.height
|
||||
);
|
||||
if (layer.mask) {
|
||||
}
|
||||
if (this.canvas.selectedLayers.includes(layer)) {
|
||||
this.drawSelectionFrame(ctx, layer);
|
||||
}
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
this.drawCanvasOutline(ctx);
|
||||
|
||||
// Renderowanie maski w zależności od trybu
|
||||
const maskImage = this.canvas.maskTool.getMask();
|
||||
if (this.canvas.maskTool.isActive) {
|
||||
// W trybie maski pokazuj maskę z przezroczystością 0.5
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.drawImage(maskImage, 0, 0);
|
||||
ctx.globalAlpha = 1.0;
|
||||
} else if (maskImage) {
|
||||
// W trybie warstw pokazuj maskę jako widoczną, ale nieedytowalną
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.globalAlpha = 1.0;
|
||||
ctx.drawImage(maskImage, 0, 0);
|
||||
ctx.globalAlpha = 1.0;
|
||||
}
|
||||
|
||||
this.renderInteractionElements(ctx);
|
||||
this.renderLayerInfo(ctx);
|
||||
|
||||
ctx.restore();
|
||||
|
||||
if (this.canvas.canvas.width !== this.canvas.offscreenCanvas.width ||
|
||||
this.canvas.canvas.height !== this.canvas.offscreenCanvas.height) {
|
||||
this.canvas.canvas.width = this.canvas.offscreenCanvas.width;
|
||||
this.canvas.canvas.height = this.canvas.offscreenCanvas.height;
|
||||
}
|
||||
this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0);
|
||||
}
|
||||
|
||||
renderInteractionElements(ctx) {
|
||||
const interaction = this.canvas.interaction;
|
||||
|
||||
if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
|
||||
const rect = interaction.canvasResizeRect;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)';
|
||||
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
||||
ctx.setLineDash([8 / this.canvas.viewport.zoom, 4 / this.canvas.viewport.zoom]);
|
||||
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
||||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
||||
const textWorldX = rect.x + rect.width / 2;
|
||||
const textWorldY = rect.y + rect.height + (20 / this.canvas.viewport.zoom);
|
||||
|
||||
ctx.save();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
const screenX = (textWorldX - this.canvas.viewport.x) * this.canvas.viewport.zoom;
|
||||
const screenY = (textWorldY - this.canvas.viewport.y) * this.canvas.viewport.zoom;
|
||||
ctx.font = "14px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const bgWidth = textMetrics.width + 10;
|
||||
const bgHeight = 22;
|
||||
ctx.fillStyle = "rgba(0, 128, 0, 0.7)";
|
||||
ctx.fillRect(screenX - bgWidth / 2, screenY - bgHeight / 2, bgWidth, bgHeight);
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText(text, screenX, screenY);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
|
||||
const rect = interaction.canvasMoveRect;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)';
|
||||
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
||||
ctx.setLineDash([10 / this.canvas.viewport.zoom, 5 / this.canvas.viewport.zoom]);
|
||||
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
||||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
|
||||
const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
|
||||
const textWorldX = rect.x + rect.width / 2;
|
||||
const textWorldY = rect.y - (20 / this.canvas.viewport.zoom);
|
||||
|
||||
ctx.save();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
const screenX = (textWorldX - this.canvas.viewport.x) * this.canvas.viewport.zoom;
|
||||
const screenY = (textWorldY - this.canvas.viewport.y) * this.canvas.viewport.zoom;
|
||||
ctx.font = "14px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const bgWidth = textMetrics.width + 10;
|
||||
const bgHeight = 22;
|
||||
ctx.fillStyle = "rgba(0, 100, 170, 0.7)";
|
||||
ctx.fillRect(screenX - bgWidth / 2, screenY - bgHeight / 2, bgWidth, bgHeight);
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText(text, screenX, screenY);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
renderLayerInfo(ctx) {
|
||||
if (this.canvas.selectedLayer) {
|
||||
this.canvas.selectedLayers.forEach(layer => {
|
||||
if (!layer.image) return;
|
||||
|
||||
const layerIndex = this.canvas.layers.indexOf(layer);
|
||||
const currentWidth = Math.round(layer.width);
|
||||
const currentHeight = Math.round(layer.height);
|
||||
const rotation = Math.round(layer.rotation % 360);
|
||||
const text = `${currentWidth}x${currentHeight} | ${rotation}° | Layer #${layerIndex + 1}`;
|
||||
|
||||
const centerX = layer.x + layer.width / 2;
|
||||
const centerY = layer.y + layer.height / 2;
|
||||
const rad = layer.rotation * Math.PI / 180;
|
||||
const cos = Math.cos(rad);
|
||||
const sin = Math.sin(rad);
|
||||
|
||||
const halfW = layer.width / 2;
|
||||
const halfH = layer.height / 2;
|
||||
|
||||
const localCorners = [
|
||||
{x: -halfW, y: -halfH},
|
||||
{x: halfW, y: -halfH},
|
||||
{x: halfW, y: halfH},
|
||||
{x: -halfW, y: halfH}
|
||||
];
|
||||
const worldCorners = localCorners.map(p => ({
|
||||
x: centerX + p.x * cos - p.y * sin,
|
||||
y: centerY + p.x * sin + p.y * cos
|
||||
}));
|
||||
let minX = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
worldCorners.forEach(p => {
|
||||
minX = Math.min(minX, p.x);
|
||||
maxX = Math.max(maxX, p.x);
|
||||
maxY = Math.max(maxY, p.y);
|
||||
});
|
||||
const padding = 20 / this.canvas.viewport.zoom;
|
||||
const textWorldX = (minX + maxX) / 2;
|
||||
const textWorldY = maxY + padding;
|
||||
ctx.save();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
|
||||
const screenX = (textWorldX - this.canvas.viewport.x) * this.canvas.viewport.zoom;
|
||||
const screenY = (textWorldY - this.canvas.viewport.y) * this.canvas.viewport.zoom;
|
||||
|
||||
ctx.font = "14px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const textBgWidth = textMetrics.width + 10;
|
||||
const textBgHeight = 22;
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
|
||||
ctx.fillRect(screenX - textBgWidth / 2, screenY - textBgHeight / 2, textBgWidth, textBgHeight);
|
||||
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText(text, screenX, screenY);
|
||||
|
||||
ctx.restore();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
drawGrid(ctx) {
|
||||
const gridSize = 64;
|
||||
const lineWidth = 0.5 / this.canvas.viewport.zoom;
|
||||
|
||||
const viewLeft = this.canvas.viewport.x;
|
||||
const viewTop = this.canvas.viewport.y;
|
||||
const viewRight = this.canvas.viewport.x + this.canvas.offscreenCanvas.width / this.canvas.viewport.zoom;
|
||||
const viewBottom = this.canvas.viewport.y + this.canvas.offscreenCanvas.height / this.canvas.viewport.zoom;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#707070';
|
||||
ctx.lineWidth = lineWidth;
|
||||
|
||||
for (let x = Math.floor(viewLeft / gridSize) * gridSize; x < viewRight; x += gridSize) {
|
||||
ctx.moveTo(x, viewTop);
|
||||
ctx.lineTo(x, viewBottom);
|
||||
}
|
||||
|
||||
for (let y = Math.floor(viewTop / gridSize) * gridSize; y < viewBottom; y += gridSize) {
|
||||
ctx.moveTo(viewLeft, y);
|
||||
ctx.lineTo(viewRight, y);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
drawCanvasOutline(ctx) {
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
ctx.lineWidth = 2 / this.canvas.viewport.zoom;
|
||||
ctx.setLineDash([10 / this.canvas.viewport.zoom, 5 / this.canvas.viewport.zoom]);
|
||||
|
||||
ctx.rect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
drawSelectionFrame(ctx, layer) {
|
||||
const lineWidth = 2 / this.canvas.viewport.zoom;
|
||||
const handleRadius = 5 / this.canvas.viewport.zoom;
|
||||
ctx.strokeStyle = '#00ff00';
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.beginPath();
|
||||
ctx.rect(-layer.width / 2, -layer.height / 2, layer.width, layer.height);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -layer.height / 2);
|
||||
ctx.lineTo(0, -layer.height / 2 - 20 / this.canvas.viewport.zoom);
|
||||
ctx.stroke();
|
||||
const handles = this.canvas.getHandles(layer);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.lineWidth = 1 / this.canvas.viewport.zoom;
|
||||
|
||||
for (const key in handles) {
|
||||
const point = handles[key];
|
||||
ctx.beginPath();
|
||||
const localX = point.x - (layer.x + layer.width / 2);
|
||||
const localY = point.y - (layer.y + layer.height / 2);
|
||||
|
||||
const rad = -layer.rotation * Math.PI / 180;
|
||||
const rotatedX = localX * Math.cos(rad) - localY * Math.sin(rad);
|
||||
const rotatedY = localX * Math.sin(rad) + localY * Math.cos(rad);
|
||||
|
||||
ctx.arc(rotatedX, rotatedY, handleRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user