Move Canvas

This commit is contained in:
Dariusz L
2025-06-21 00:15:51 +02:00
parent 3e1e8bb372
commit 8cd0716449

View File

@@ -10,7 +10,7 @@ export class Canvas {
this.selectedLayer = null; this.selectedLayer = null;
this.selectedLayers = []; this.selectedLayers = [];
this.onSelectionChange = null; this.onSelectionChange = null;
this.lastMousePosition = { x: 0, y: 0 }; this.lastMousePosition = {x: 0, y: 0};
this.viewport = { this.viewport = {
x: -(this.width / 4), x: -(this.width / 4),
@@ -31,7 +31,8 @@ export class Canvas {
lastClickTime: 0, lastClickTime: 0,
}; };
this.originalLayerPositions = new Map(); this.originalLayerPositions = new Map();
this.canvasResizeRect = null; this.interaction.canvasResizeRect = null;
this.interaction.canvasMoveRect = null;
this.offscreenCanvas = document.createElement('canvas'); this.offscreenCanvas = document.createElement('canvas');
this.offscreenCtx = this.offscreenCanvas.getContext('2d', { this.offscreenCtx = this.offscreenCanvas.getContext('2d', {
@@ -94,8 +95,12 @@ export class Canvas {
document.addEventListener('keydown', this.handleKeyDown.bind(this)); document.addEventListener('keydown', this.handleKeyDown.bind(this));
document.addEventListener('keyup', this.handleKeyUp.bind(this)); document.addEventListener('keyup', this.handleKeyUp.bind(this));
this.canvas.addEventListener('mouseenter', () => { this.isMouseOver = true; }); this.canvas.addEventListener('mouseenter', () => {
this.canvas.addEventListener('mouseleave', () => { this.isMouseOver = false; }); this.isMouseOver = true;
});
this.canvas.addEventListener('mouseleave', () => {
this.isMouseOver = false;
});
} }
updateSelection(newSelection) { updateSelection(newSelection) {
@@ -113,7 +118,8 @@ export class Canvas {
this.interaction.mode = 'none'; this.interaction.mode = 'none';
this.interaction.resizeHandle = null; this.interaction.resizeHandle = null;
this.originalLayerPositions.clear(); this.originalLayerPositions.clear();
this.canvasResizeRect = null; this.interaction.canvasResizeRect = null;
this.interaction.canvasMoveRect = null;
this.interaction.hasClonedInDrag = false; this.interaction.hasClonedInDrag = false;
this.canvas.style.cursor = 'default'; this.canvas.style.cursor = 'default';
} }
@@ -124,6 +130,12 @@ export class Canvas {
handleMouseDown(e) { handleMouseDown(e) {
const currentTime = Date.now(); const currentTime = Date.now();
const worldCoords = this.getMouseWorldCoordinates(e); const worldCoords = this.getMouseWorldCoordinates(e);
if (e.shiftKey && e.ctrlKey) {
this.startCanvasMove(worldCoords);
this.render();
return;
}
if (currentTime - this.interaction.lastClickTime < 300) { if (currentTime - this.interaction.lastClickTime < 300) {
this.updateSelection([]); this.updateSelection([]);
this.selectedLayer = null; this.selectedLayer = null;
@@ -163,23 +175,17 @@ export class Canvas {
*/ */
async copySelectedLayers() { async copySelectedLayers() {
if (this.selectedLayers.length === 0) return; if (this.selectedLayers.length === 0) return;
this.internalClipboard = this.selectedLayers.map(layer => ({...layer}));
// 1. Kopiowanie do wewnętrznego schowka (bez zmian)
this.internalClipboard = this.selectedLayers.map(layer => ({ ...layer }));
console.log(`Copied ${this.internalClipboard.length} layer(s) to internal clipboard.`); console.log(`Copied ${this.internalClipboard.length} layer(s) to internal clipboard.`);
// 2. Kopiowanie spłaszczonego obrazu do globalnego schowka
try { try {
const blob = await this.getFlattenedSelectionAsBlob(); const blob = await this.getFlattenedSelectionAsBlob();
if (blob) { if (blob) {
// Używamy Clipboard API do wstawienia obrazka const item = new ClipboardItem({'image/png': blob});
const item = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([item]); await navigator.clipboard.write([item]);
console.log("Flattened selection copied to the system clipboard."); console.log("Flattened selection copied to the system clipboard.");
} }
} catch (error) { } catch (error) {
console.error("Failed to copy image to system clipboard:", error); console.error("Failed to copy image to system clipboard:", error);
// Można tu dodać powiadomienie dla użytkownika, jeśli operacja się nie uda
} }
} }
@@ -190,35 +196,34 @@ export class Canvas {
if (this.internalClipboard.length === 0) return; if (this.internalClipboard.length === 0) return;
const newLayers = []; const newLayers = [];
const pasteOffset = 20; // Przesunięcie wklejonych warstw const pasteOffset = 20;
this.internalClipboard.forEach(clipboardLayer => { this.internalClipboard.forEach(clipboardLayer => {
const newLayer = { const newLayer = {
...clipboardLayer, ...clipboardLayer,
x: clipboardLayer.x + pasteOffset / this.viewport.zoom, x: clipboardLayer.x + pasteOffset / this.viewport.zoom,
y: clipboardLayer.y + pasteOffset / this.viewport.zoom, y: clipboardLayer.y + pasteOffset / this.viewport.zoom,
zIndex: this.layers.length // Upewnij się, że nowa warstwa jest na wierzchu zIndex: this.layers.length
}; };
this.layers.push(newLayer); this.layers.push(newLayer);
newLayers.push(newLayer); newLayers.push(newLayer);
}); });
this.updateSelection(newLayers); // Zaznacz nowo wklejone warstwy this.updateSelection(newLayers);
this.render(); this.render();
console.log(`Pasted ${newLayers.length} layer(s).`); console.log(`Pasted ${newLayers.length} layer(s).`);
} }
/** /**
* Inteligentnie obsługuje operację wklejania. * Inteligentnie obsługuje operację wklejania.
* Najpierw próbuje wkleić obraz z globalnego schowka, * Najpierw próbuje wkleić obraz z globalnego schowka,
* a jeśli to się nie uda, wkleja z wewnętrznego schowka. * a jeśli to się nie uda, wkleja z wewnętrznego schowka.
*/ */
async handlePaste() { async handlePaste() {
try { try {
// Sprawdź, czy przeglądarka obsługuje API schowka
if (!navigator.clipboard?.read) { if (!navigator.clipboard?.read) {
console.log("Browser does not support clipboard read API. Falling back to internal paste."); console.log("Browser does not support clipboard read API. Falling back to internal paste.");
this.pasteLayers(); // Fallback do wklejania wewnętrznego this.pasteLayers();
return; return;
} }
@@ -232,40 +237,33 @@ export class Canvas {
const blob = await item.getType(imageType); const blob = await item.getType(imageType);
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
// Tworzenie nowej warstwy z obrazka ze schowka
const newLayer = { const newLayer = {
image: img, image: img,
// Wklej obrazek tak, aby jego środek był pod kursorem
x: this.lastMousePosition.x - img.width / 2, x: this.lastMousePosition.x - img.width / 2,
y: this.lastMousePosition.y - img.height / 2, y: this.lastMousePosition.y - img.height / 2,
width: img.width, // Oryginalna szerokość width: img.width,
height: img.height, // Oryginalna wysokość height: img.height,
rotation: 0, rotation: 0,
zIndex: this.layers.length, zIndex: this.layers.length,
blendMode: 'normal', blendMode: 'normal',
opacity: 1 opacity: 1
}; };
this.layers.push(newLayer); this.layers.push(newLayer);
this.updateSelection([newLayer]); // Zaznacz nową warstwę this.updateSelection([newLayer]);
this.render(); this.render();
// Zwolnij zasoby, aby uniknąć wycieków pamięci
URL.revokeObjectURL(img.src); URL.revokeObjectURL(img.src);
}; };
img.src = URL.createObjectURL(blob); img.src = URL.createObjectURL(blob);
imagePasted = true; imagePasted = true;
break; // Znaleziono i przetworzono obraz, przerwij pętlę break;
} }
} }
// Jeśli żaden obraz nie został wklejony z globalnego schowka, użyj naszego wewnętrznego
if (!imagePasted) { if (!imagePasted) {
this.pasteLayers(); this.pasteLayers();
} }
} catch (err) { } catch (err) {
console.error("Paste operation failed, falling back to internal paste. Error:", err); console.error("Paste operation failed, falling back to internal paste. Error:", err);
// Błąd (np. brak uprawnień) również powinien skutkować próbą wklejenia z wewnętrznego schowka
this.pasteLayers(); this.pasteLayers();
} }
} }
@@ -275,7 +273,7 @@ export class Canvas {
*/ */
handleMouseMove(e) { handleMouseMove(e) {
const worldCoords = this.getMouseWorldCoordinates(e); const worldCoords = this.getMouseWorldCoordinates(e);
this.lastMousePosition = worldCoords; // Zapisujemy ostatnią pozycję kursora this.lastMousePosition = worldCoords;
switch (this.interaction.mode) { switch (this.interaction.mode) {
case 'panning': case 'panning':
@@ -293,6 +291,9 @@ export class Canvas {
case 'resizingCanvas': case 'resizingCanvas':
this.updateCanvasResize(worldCoords); this.updateCanvasResize(worldCoords);
break; break;
case 'movingCanvas':
this.updateCanvasMove(worldCoords);
break;
default: default:
this.updateCursor(worldCoords); this.updateCursor(worldCoords);
break; break;
@@ -305,6 +306,8 @@ export class Canvas {
handleMouseUp(e) { handleMouseUp(e) {
if (this.interaction.mode === 'resizingCanvas') { if (this.interaction.mode === 'resizingCanvas') {
this.finalizeCanvasResize(); this.finalizeCanvasResize();
} else if (this.interaction.mode === 'movingCanvas') {
this.finalizeCanvasMove();
} }
this.resetInteractionState(); this.resetInteractionState();
this.render(); this.render();
@@ -401,9 +404,7 @@ export class Canvas {
* Metoda obsługująca wciśnięcie klawisza. * Metoda obsługująca wciśnięcie klawisza.
*/ */
handleKeyDown(e) { handleKeyDown(e) {
// Przechwytywanie Ctrl+C i Ctrl+V tylko jeśli kursor jest nad płótnem
if (this.isMouseOver) { if (this.isMouseOver) {
// Kopiowanie (Ctrl+C)
if (e.ctrlKey && e.key.toLowerCase() === 'c') { if (e.ctrlKey && e.key.toLowerCase() === 'c') {
if (this.selectedLayers.length > 0) { if (this.selectedLayers.length > 0) {
e.preventDefault(); e.preventDefault();
@@ -412,12 +413,10 @@ export class Canvas {
return; return;
} }
} }
// Wklejanie (Ctrl+V)
if (e.ctrlKey && e.key.toLowerCase() === 'v') { if (e.ctrlKey && e.key.toLowerCase() === 'v') {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.handlePaste(); // Wywołujemy naszą nową, inteligentną funkcję this.handlePaste();
return; return;
} }
} }
@@ -472,6 +471,7 @@ export class Canvas {
} }
} }
} }
/** /**
* Metoda obsługująca puszczenie klawisza. * Metoda obsługująca puszczenie klawisza.
*/ */
@@ -554,7 +554,59 @@ export class Canvas {
const startX = this.snapToGrid(worldCoords.x); const startX = this.snapToGrid(worldCoords.x);
const startY = this.snapToGrid(worldCoords.y); const startY = this.snapToGrid(worldCoords.y);
this.interaction.canvasResizeStart = {x: startX, y: startY}; this.interaction.canvasResizeStart = {x: startX, y: startY};
this.canvasResizeRect = {x: startX, y: startY, width: 0, height: 0}; this.interaction.canvasResizeRect = {x: startX, y: startY, width: 0, height: 0};
this.render();
}
startCanvasMove(worldCoords) {
this.interaction.mode = 'movingCanvas';
this.interaction.dragStart = { ...worldCoords };
const initialX = this.snapToGrid(worldCoords.x - this.width / 2);
const initialY = this.snapToGrid(worldCoords.y - this.height / 2);
this.interaction.canvasMoveRect = {
x: initialX,
y: initialY,
width: this.width,
height: this.height
};
this.canvas.style.cursor = 'grabbing';
this.render();
}
/**
* Aktualizuje pozycję "ducha" płótna podczas przesuwania.
*/
updateCanvasMove(worldCoords) {
if (!this.interaction.canvasMoveRect) return;
const dx = worldCoords.x - this.interaction.dragStart.x;
const dy = worldCoords.y - this.interaction.dragStart.y;
const initialRectX = this.snapToGrid(this.interaction.dragStart.x - this.width / 2);
const initialRectY = this.snapToGrid(this.interaction.dragStart.y - this.height / 2);
this.interaction.canvasMoveRect.x = this.snapToGrid(initialRectX + dx);
this.interaction.canvasMoveRect.y = this.snapToGrid(initialRectY + dy);
this.render();
}
/**
* Kończy przesuwanie płótna i zatwierdza nową pozycję.
*/
finalizeCanvasMove() {
const moveRect = this.interaction.canvasMoveRect;
if (moveRect && (moveRect.x !== 0 || moveRect.y !== 0)) {
const finalX = moveRect.x;
const finalY = moveRect.y;
this.layers.forEach(layer => {
layer.x -= finalX;
layer.y -= finalY;
});
this.viewport.x -= finalX;
this.viewport.y -= finalY;
}
this.render(); this.render();
} }
@@ -701,19 +753,19 @@ export class Canvas {
const snappedMouseY = this.snapToGrid(worldCoords.y); const snappedMouseY = this.snapToGrid(worldCoords.y);
const start = this.interaction.canvasResizeStart; const start = this.interaction.canvasResizeStart;
this.canvasResizeRect.x = Math.min(snappedMouseX, start.x); this.interaction.canvasResizeRect.x = Math.min(snappedMouseX, start.x);
this.canvasResizeRect.y = Math.min(snappedMouseY, start.y); this.interaction.canvasResizeRect.y = Math.min(snappedMouseY, start.y);
this.canvasResizeRect.width = Math.abs(snappedMouseX - start.x); this.interaction.canvasResizeRect.width = Math.abs(snappedMouseX - start.x);
this.canvasResizeRect.height = Math.abs(snappedMouseY - start.y); this.interaction.canvasResizeRect.height = Math.abs(snappedMouseY - start.y);
this.render(); this.render();
} }
finalizeCanvasResize() { finalizeCanvasResize() {
if (this.canvasResizeRect && this.canvasResizeRect.width > 1 && this.canvasResizeRect.height > 1) { if (this.interaction.canvasResizeRect && this.interaction.canvasResizeRect.width > 1 && this.interaction.canvasResizeRect.height > 1) {
const newWidth = Math.round(this.canvasResizeRect.width); const newWidth = Math.round(this.interaction.canvasResizeRect.width);
const newHeight = Math.round(this.canvasResizeRect.height); const newHeight = Math.round(this.interaction.canvasResizeRect.height);
const rectX = this.canvasResizeRect.x; const rectX = this.interaction.canvasResizeRect.x;
const rectY = this.canvasResizeRect.y; const rectY = this.interaction.canvasResizeRect.y;
this.updateCanvasSize(newWidth, newHeight); this.updateCanvasSize(newWidth, newHeight);
@@ -932,10 +984,10 @@ export class Canvas {
} }
ctx.restore(); ctx.restore();
}); });
this.drawCanvasOutline(ctx);
if (this.interaction.mode === 'resizingCanvas' && this.canvasResizeRect) { this.drawCanvasOutline(ctx);
const rect = this.canvasResizeRect; if (this.interaction.mode === 'resizingCanvas' && this.interaction.canvasResizeRect) {
const rect = this.interaction.canvasResizeRect;
ctx.save(); ctx.save();
ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)'; ctx.strokeStyle = 'rgba(0, 255, 0, 0.8)';
ctx.lineWidth = 2 / this.viewport.zoom; ctx.lineWidth = 2 / this.viewport.zoom;
@@ -950,26 +1002,51 @@ export class Canvas {
ctx.save(); ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.setTransform(1, 0, 0, 1, 0, 0);
const screenX = (textWorldX - this.viewport.x) * this.viewport.zoom; const screenX = (textWorldX - this.viewport.x) * this.viewport.zoom;
const screenY = (textWorldY - this.viewport.y) * this.viewport.zoom; const screenY = (textWorldY - this.viewport.y) * this.viewport.zoom;
ctx.font = "14px sans-serif"; ctx.font = "14px sans-serif";
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.textBaseline = "middle"; ctx.textBaseline = "middle";
const textMetrics = ctx.measureText(text); const textMetrics = ctx.measureText(text);
const bgWidth = textMetrics.width + 10; const bgWidth = textMetrics.width + 10;
const bgHeight = 22; const bgHeight = 22;
ctx.fillStyle = "rgba(0, 128, 0, 0.7)"; ctx.fillStyle = "rgba(0, 128, 0, 0.7)";
ctx.fillRect(screenX - bgWidth / 2, screenY - bgHeight / 2, bgWidth, bgHeight); ctx.fillRect(screenX - bgWidth / 2, screenY - bgHeight / 2, bgWidth, bgHeight);
ctx.fillStyle = "white"; ctx.fillStyle = "white";
ctx.fillText(text, screenX, screenY); ctx.fillText(text, screenX, screenY);
ctx.restore(); 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) { if (this.selectedLayer) {
this.selectedLayers.forEach(layer => { this.selectedLayers.forEach(layer => {
@@ -1361,7 +1438,7 @@ export class Canvas {
} }
/** /**
* Tworzy spłaszczony obraz z zaznaczonych warstw, przycięty do ich zawartości. * Tworzy spłaszczony obraz z zaznaczonych warstw, przycięty do ich zawartości.
* @returns {Promise<Blob|null>} Obiekt Blob z obrazem PNG lub null, jeśli nic nie jest zaznaczone. * @returns {Promise<Blob|null>} Obiekt Blob z obrazem PNG lub null, jeśli nic nie jest zaznaczone.
*/ */
@@ -1372,8 +1449,6 @@ export class Canvas {
return new Promise((resolve) => { return new Promise((resolve) => {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
// 1. Oblicz bounding box dla wszystkich zaznaczonych i obróconych warstw
this.selectedLayers.forEach(layer => { this.selectedLayers.forEach(layer => {
const centerX = layer.x + layer.width / 2; const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2; const centerY = layer.y + layer.height / 2;
@@ -1385,10 +1460,10 @@ export class Canvas {
const halfH = layer.height / 2; const halfH = layer.height / 2;
const corners = [ const corners = [
{ x: -halfW, y: -halfH }, {x: -halfW, y: -halfH},
{ x: halfW, y: -halfH }, {x: halfW, y: -halfH},
{ x: halfW, y: halfH }, {x: halfW, y: halfH},
{ x: -halfW, y: halfH } {x: -halfW, y: halfH}
]; ];
corners.forEach(p => { corners.forEach(p => {
@@ -1409,15 +1484,11 @@ export class Canvas {
resolve(null); resolve(null);
return; return;
} }
// 2. Stwórz tymczasowe płótno o wymiarach bounding boxa
const tempCanvas = document.createElement('canvas'); const tempCanvas = document.createElement('canvas');
tempCanvas.width = newWidth; tempCanvas.width = newWidth;
tempCanvas.height = newHeight; tempCanvas.height = newHeight;
const tempCtx = tempCanvas.getContext('2d'); const tempCtx = tempCanvas.getContext('2d');
// 3. Narysuj zaznaczone warstwy na nowym płótnie
// Przesuwamy cały układ współrzędnych, aby lewy górny róg bounding boxa był w (0,0)
tempCtx.translate(-minX, -minY); tempCtx.translate(-minX, -minY);
const sortedSelection = [...this.selectedLayers].sort((a, b) => a.zIndex - b.zIndex); const sortedSelection = [...this.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
@@ -1440,8 +1511,6 @@ export class Canvas {
); );
tempCtx.restore(); tempCtx.restore();
}); });
// 4. Konwertuj płótno na Blob
tempCanvas.toBlob((blob) => { tempCanvas.toBlob((blob) => {
resolve(blob); resolve(blob);
}, 'image/png'); }, 'image/png');