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.selectedLayers = [];
this.onSelectionChange = null;
this.lastMousePosition = { x: 0, y: 0 };
this.lastMousePosition = {x: 0, y: 0};
this.viewport = {
x: -(this.width / 4),
@@ -31,7 +31,8 @@ export class Canvas {
lastClickTime: 0,
};
this.originalLayerPositions = new Map();
this.canvasResizeRect = null;
this.interaction.canvasResizeRect = null;
this.interaction.canvasMoveRect = null;
this.offscreenCanvas = document.createElement('canvas');
this.offscreenCtx = this.offscreenCanvas.getContext('2d', {
@@ -94,8 +95,12 @@ export class Canvas {
document.addEventListener('keydown', this.handleKeyDown.bind(this));
document.addEventListener('keyup', this.handleKeyUp.bind(this));
this.canvas.addEventListener('mouseenter', () => { this.isMouseOver = true; });
this.canvas.addEventListener('mouseleave', () => { this.isMouseOver = false; });
this.canvas.addEventListener('mouseenter', () => {
this.isMouseOver = true;
});
this.canvas.addEventListener('mouseleave', () => {
this.isMouseOver = false;
});
}
updateSelection(newSelection) {
@@ -113,7 +118,8 @@ export class Canvas {
this.interaction.mode = 'none';
this.interaction.resizeHandle = null;
this.originalLayerPositions.clear();
this.canvasResizeRect = null;
this.interaction.canvasResizeRect = null;
this.interaction.canvasMoveRect = null;
this.interaction.hasClonedInDrag = false;
this.canvas.style.cursor = 'default';
}
@@ -124,6 +130,12 @@ export class Canvas {
handleMouseDown(e) {
const currentTime = Date.now();
const worldCoords = this.getMouseWorldCoordinates(e);
if (e.shiftKey && e.ctrlKey) {
this.startCanvasMove(worldCoords);
this.render();
return;
}
if (currentTime - this.interaction.lastClickTime < 300) {
this.updateSelection([]);
this.selectedLayer = null;
@@ -163,23 +175,17 @@ export class Canvas {
*/
async copySelectedLayers() {
if (this.selectedLayers.length === 0) return;
// 1. Kopiowanie do wewnętrznego schowka (bez zmian)
this.internalClipboard = this.selectedLayers.map(layer => ({ ...layer }));
this.internalClipboard = this.selectedLayers.map(layer => ({...layer}));
console.log(`Copied ${this.internalClipboard.length} layer(s) to internal clipboard.`);
// 2. Kopiowanie spłaszczonego obrazu do globalnego schowka
try {
const blob = await this.getFlattenedSelectionAsBlob();
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]);
console.log("Flattened selection copied to the system clipboard.");
}
} catch (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;
const newLayers = [];
const pasteOffset = 20; // Przesunięcie wklejonych warstw
const pasteOffset = 20;
this.internalClipboard.forEach(clipboardLayer => {
const newLayer = {
...clipboardLayer,
x: clipboardLayer.x + 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);
newLayers.push(newLayer);
});
this.updateSelection(newLayers); // Zaznacz nowo wklejone warstwy
this.updateSelection(newLayers);
this.render();
console.log(`Pasted ${newLayers.length} layer(s).`);
}
/**
/**
* Inteligentnie obsługuje operację wklejania.
* Najpierw próbuje wkleić obraz z globalnego schowka,
* a jeśli to się nie uda, wkleja z wewnętrznego schowka.
*/
async handlePaste() {
try {
// Sprawdź, czy przeglądarka obsługuje API schowka
if (!navigator.clipboard?.read) {
console.log("Browser does not support clipboard read API. Falling back to internal paste.");
this.pasteLayers(); // Fallback do wklejania wewnętrznego
this.pasteLayers();
return;
}
@@ -232,40 +237,33 @@ export class Canvas {
const blob = await item.getType(imageType);
const img = new Image();
img.onload = () => {
// Tworzenie nowej warstwy z obrazka ze schowka
const newLayer = {
image: img,
// Wklej obrazek tak, aby jego środek był pod kursorem
x: this.lastMousePosition.x - img.width / 2,
y: this.lastMousePosition.y - img.height / 2,
width: img.width, // Oryginalna szerokość
height: img.height, // Oryginalna wysokość
width: img.width,
height: img.height,
rotation: 0,
zIndex: this.layers.length,
blendMode: 'normal',
opacity: 1
};
this.layers.push(newLayer);
this.updateSelection([newLayer]); // Zaznacz nową warstwę
this.updateSelection([newLayer]);
this.render();
// Zwolnij zasoby, aby uniknąć wycieków pamięci
URL.revokeObjectURL(img.src);
};
img.src = URL.createObjectURL(blob);
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) {
this.pasteLayers();
}
} catch (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();
}
}
@@ -275,7 +273,7 @@ export class Canvas {
*/
handleMouseMove(e) {
const worldCoords = this.getMouseWorldCoordinates(e);
this.lastMousePosition = worldCoords; // Zapisujemy ostatnią pozycję kursora
this.lastMousePosition = worldCoords;
switch (this.interaction.mode) {
case 'panning':
@@ -293,6 +291,9 @@ export class Canvas {
case 'resizingCanvas':
this.updateCanvasResize(worldCoords);
break;
case 'movingCanvas':
this.updateCanvasMove(worldCoords);
break;
default:
this.updateCursor(worldCoords);
break;
@@ -305,6 +306,8 @@ export class Canvas {
handleMouseUp(e) {
if (this.interaction.mode === 'resizingCanvas') {
this.finalizeCanvasResize();
} else if (this.interaction.mode === 'movingCanvas') {
this.finalizeCanvasMove();
}
this.resetInteractionState();
this.render();
@@ -401,9 +404,7 @@ export class Canvas {
* Metoda obsługująca wciśnięcie klawisza.
*/
handleKeyDown(e) {
// Przechwytywanie Ctrl+C i Ctrl+V tylko jeśli kursor jest nad płótnem
if (this.isMouseOver) {
// Kopiowanie (Ctrl+C)
if (e.ctrlKey && e.key.toLowerCase() === 'c') {
if (this.selectedLayers.length > 0) {
e.preventDefault();
@@ -412,12 +413,10 @@ export class Canvas {
return;
}
}
// Wklejanie (Ctrl+V)
if (e.ctrlKey && e.key.toLowerCase() === 'v') {
e.preventDefault();
e.stopPropagation();
this.handlePaste(); // Wywołujemy naszą nową, inteligentną funkcję
this.handlePaste();
return;
}
}
@@ -472,6 +471,7 @@ export class Canvas {
}
}
}
/**
* Metoda obsługująca puszczenie klawisza.
*/
@@ -554,7 +554,59 @@ export class Canvas {
const startX = this.snapToGrid(worldCoords.x);
const startY = this.snapToGrid(worldCoords.y);
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();
}
@@ -701,19 +753,19 @@ export class Canvas {
const snappedMouseY = this.snapToGrid(worldCoords.y);
const start = this.interaction.canvasResizeStart;
this.canvasResizeRect.x = Math.min(snappedMouseX, start.x);
this.canvasResizeRect.y = Math.min(snappedMouseY, start.y);
this.canvasResizeRect.width = Math.abs(snappedMouseX - start.x);
this.canvasResizeRect.height = Math.abs(snappedMouseY - start.y);
this.interaction.canvasResizeRect.x = Math.min(snappedMouseX, start.x);
this.interaction.canvasResizeRect.y = Math.min(snappedMouseY, start.y);
this.interaction.canvasResizeRect.width = Math.abs(snappedMouseX - start.x);
this.interaction.canvasResizeRect.height = Math.abs(snappedMouseY - start.y);
this.render();
}
finalizeCanvasResize() {
if (this.canvasResizeRect && this.canvasResizeRect.width > 1 && this.canvasResizeRect.height > 1) {
const newWidth = Math.round(this.canvasResizeRect.width);
const newHeight = Math.round(this.canvasResizeRect.height);
const rectX = this.canvasResizeRect.x;
const rectY = this.canvasResizeRect.y;
if (this.interaction.canvasResizeRect && this.interaction.canvasResizeRect.width > 1 && this.interaction.canvasResizeRect.height > 1) {
const newWidth = Math.round(this.interaction.canvasResizeRect.width);
const newHeight = Math.round(this.interaction.canvasResizeRect.height);
const rectX = this.interaction.canvasResizeRect.x;
const rectY = this.interaction.canvasResizeRect.y;
this.updateCanvasSize(newWidth, newHeight);
@@ -932,10 +984,10 @@ export class Canvas {
}
ctx.restore();
});
this.drawCanvasOutline(ctx);
if (this.interaction.mode === 'resizingCanvas' && this.canvasResizeRect) {
const rect = this.canvasResizeRect;
this.drawCanvasOutline(ctx);
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;
@@ -950,26 +1002,51 @@ export class Canvas {
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 => {
@@ -1361,7 +1438,7 @@ export class Canvas {
}
/**
/**
* 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.
*/
@@ -1372,8 +1449,6 @@ export class Canvas {
return new Promise((resolve) => {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
// 1. Oblicz bounding box dla wszystkich zaznaczonych i obróconych warstw
this.selectedLayers.forEach(layer => {
const centerX = layer.x + layer.width / 2;
const centerY = layer.y + layer.height / 2;
@@ -1385,10 +1460,10 @@ export class Canvas {
const halfH = layer.height / 2;
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 => {
@@ -1409,15 +1484,11 @@ export class Canvas {
resolve(null);
return;
}
// 2. Stwórz tymczasowe płótno o wymiarach bounding boxa
const tempCanvas = document.createElement('canvas');
tempCanvas.width = newWidth;
tempCanvas.height = newHeight;
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);
const sortedSelection = [...this.selectedLayers].sort((a, b) => a.zIndex - b.zIndex);
@@ -1440,8 +1511,6 @@ export class Canvas {
);
tempCtx.restore();
});
// 4. Konwertuj płótno na Blob
tempCanvas.toBlob((blob) => {
resolve(blob);
}, 'image/png');