diff --git a/js/Canvas.js b/js/Canvas.js index e80d60c..047e63c 100644 --- a/js/Canvas.js +++ b/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) { diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js new file mode 100644 index 0000000..4ab2563 --- /dev/null +++ b/js/CanvasRenderer.js @@ -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(); + } + } +}