[FEAT] Multi select, flip, rotate, multi remove

This commit is contained in:
2025-12-17 21:08:43 +01:00
parent 3aa01dd044
commit 6fe90c6af9
10 changed files with 449 additions and 193 deletions

View File

@@ -55,7 +55,18 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
const row = Math.floor(index / columns.value);
const cellX = Math.floor(col * cellWidth);
const cellY = Math.floor(row * cellHeight);
ctx.drawImage(sprite.img, Math.floor(cellX + negativeSpacing + sprite.x), Math.floor(cellY + negativeSpacing + sprite.y));
if (sprite.rotation || sprite.flipX || sprite.flipY) {
ctx.save();
const centerX = Math.floor(cellX + negativeSpacing + sprite.x + sprite.width / 2);
const centerY = Math.floor(cellY + negativeSpacing + sprite.y + sprite.height / 2);
ctx.translate(centerX, centerY);
ctx.rotate((sprite.rotation * Math.PI) / 180);
ctx.scale(sprite.flipX ? -1 : 1, sprite.flipY ? -1 : 1);
ctx.drawImage(sprite.img, -sprite.width / 2, -sprite.height / 2);
ctx.restore();
} else {
ctx.drawImage(sprite.img, Math.floor(cellX + negativeSpacing + sprite.x), Math.floor(cellY + negativeSpacing + sprite.y));
}
});
const link = document.createElement('a');
@@ -77,7 +88,15 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
if (!ctx) return null;
canvas.width = sprite.width;
canvas.height = sprite.height;
ctx.drawImage(sprite.img, 0, 0);
if (sprite.rotation) {
ctx.save();
ctx.translate(sprite.width / 2, sprite.height / 2);
ctx.rotate((sprite.rotation * Math.PI) / 180);
ctx.drawImage(sprite.img, -sprite.width / 2, -sprite.height / 2);
ctx.restore();
} else {
ctx.drawImage(sprite.img, 0, 0);
}
const base64Data = canvas.toDataURL('image/png');
return {
id: sprite.id,
@@ -85,6 +104,9 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
height: sprite.height,
x: sprite.x,
y: sprite.y,
rotation: sprite.rotation || 0,
flipX: sprite.flipX || false,
flipY: sprite.flipY || false,
base64: base64Data,
name: sprite.file.name,
};
@@ -156,6 +178,9 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
height: spriteData.height,
x: spriteData.x || 0,
y: spriteData.y || 0,
rotation: spriteData.rotation || 0,
flipX: spriteData.flipX || false,
flipY: spriteData.flipY || false,
});
};
img.src = spriteData.base64;
@@ -189,7 +214,17 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
ctx.fillStyle = backgroundColor.value;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y));
if (sprite.rotation) {
ctx.save();
const centerX = Math.floor(negativeSpacing + sprite.x + sprite.width / 2);
const centerY = Math.floor(negativeSpacing + sprite.y + sprite.height / 2);
ctx.translate(centerX, centerY);
ctx.rotate((sprite.rotation * Math.PI) / 180);
ctx.drawImage(sprite.img, -sprite.width / 2, -sprite.height / 2);
ctx.restore();
} else {
ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y));
}
gif.addFrame(ctx, { copy: true, delay: 1000 / fps });
});
@@ -227,7 +262,17 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
ctx.fillStyle = backgroundColor.value;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y));
if (sprite.rotation) {
ctx.save();
const centerX = Math.floor(negativeSpacing + sprite.x + sprite.width / 2);
const centerY = Math.floor(negativeSpacing + sprite.y + sprite.height / 2);
ctx.translate(centerX, centerY);
ctx.rotate((sprite.rotation * Math.PI) / 180);
ctx.drawImage(sprite.img, -sprite.width / 2, -sprite.height / 2);
ctx.restore();
} else {
ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y));
}
const dataURL = canvas.toDataURL('image/png');
const binary = atob(dataURL.split(',')[1]);
const buf = new ArrayBuffer(binary.length);

View File

@@ -42,7 +42,19 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
vLayers.forEach(layer => {
const sprite = layer.sprites[cellIndex];
if (!sprite) return;
ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y));
if (sprite.rotation || sprite.flipX || sprite.flipY) {
ctx.save();
const centerX = Math.floor(negativeSpacing + sprite.x + sprite.width / 2);
const centerY = Math.floor(negativeSpacing + sprite.y + sprite.height / 2);
ctx.translate(centerX, centerY);
ctx.rotate((sprite.rotation * Math.PI) / 180);
ctx.scale(sprite.flipX ? -1 : 1, sprite.flipY ? -1 : 1);
ctx.drawImage(sprite.img, -sprite.width / 2, -sprite.height / 2);
ctx.restore();
} else {
ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y));
}
});
};
@@ -110,7 +122,7 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
canvas.height = sprite.height;
ctx.drawImage(sprite.img, 0, 0);
const base64 = canvas.toDataURL('image/png');
return { id: sprite.id, width: sprite.width, height: sprite.height, x: sprite.x, y: sprite.y, base64, name: sprite.file.name };
return { id: sprite.id, width: sprite.width, height: sprite.height, x: sprite.x, y: sprite.y, rotation: sprite.rotation, flipX: sprite.flipX, flipY: sprite.flipY, base64, name: sprite.file.name };
})
);
return { id: layer.id, name: layer.name, visible: layer.visible, locked: layer.locked, sprites: sprites.filter(Boolean) };
@@ -153,7 +165,7 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
const blob = new Blob([ab], { type: mimeType });
const fileName = spriteData.name || `sprite-${spriteData.id}.png`;
const file = new File([blob], fileName, { type: mimeType });
resolve({ id: spriteData.id || crypto.randomUUID(), file, img, url: spriteData.base64, width: spriteData.width, height: spriteData.height, x: spriteData.x || 0, y: spriteData.y || 0 });
resolve({ id: spriteData.id || crypto.randomUUID(), file, img, url: spriteData.base64, width: spriteData.width, height: spriteData.height, x: spriteData.x || 0, y: spriteData.y || 0, rotation: spriteData.rotation || 0, flipX: spriteData.flipX || false, flipY: spriteData.flipY || false });
};
img.src = spriteData.base64;
});
@@ -273,7 +285,7 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
name: layer.name,
visible: layer.visible,
locked: layer.locked,
sprites: await Promise.all(layer.sprites.map(async s => ({ id: s.id, width: s.width, height: s.height, x: s.x, y: s.y, name: s.file.name }))),
sprites: await Promise.all(layer.sprites.map(async s => ({ id: s.id, width: s.width, height: s.height, x: s.x, y: s.y, rotation: s.rotation, flipX: s.flipX, flipY: s.flipY, name: s.file.name }))),
}))
);
const meta = {

View File

@@ -130,6 +130,49 @@ export const useLayers = () => {
l.sprites.splice(i, 1);
};
const removeSprites = (ids: string[]) => {
const l = activeLayer.value;
if (!l) return;
// Sort indices in descending order to avoid shift issues when splicing
const indicesToRemove: number[] = [];
ids.forEach(id => {
const i = l.sprites.findIndex(s => s.id === id);
if (i !== -1) indicesToRemove.push(i);
});
indicesToRemove.sort((a, b) => b - a);
indicesToRemove.forEach(i => {
const s = l.sprites[i];
if (s.url && s.url.startsWith('blob:')) {
try {
URL.revokeObjectURL(s.url);
} catch {}
}
l.sprites.splice(i, 1);
});
};
const rotateSprite = (id: string, angle: number) => {
const l = activeLayer.value;
if (!l) return;
const s = l.sprites.find(s => s.id === id);
if (s) {
s.rotation = (s.rotation + angle) % 360;
}
};
const flipSprite = (id: string, direction: 'horizontal' | 'vertical') => {
const l = activeLayer.value;
if (!l) return;
const s = l.sprites.find(s => s.id === id);
if (s) {
if (direction === 'horizontal') s.flipX = !s.flipX;
if (direction === 'vertical') s.flipY = !s.flipY;
}
};
const replaceSprite = (id: string, file: File) => {
const l = activeLayer.value;
if (!l) return;
@@ -147,7 +190,7 @@ export const useLayers = () => {
const url = e.target?.result as string;
const img = new Image();
img.onload = () => {
l.sprites[i] = { id: old.id, file, img, url, width: img.width, height: img.height, x: old.x, y: old.y };
l.sprites[i] = { id: old.id, file, img, url, width: img.width, height: img.height, x: old.x, y: old.y, rotation: old.rotation, flipX: old.flipX || false, flipY: old.flipY || false };
};
img.onerror = () => {
console.error('Failed to load replacement image:', file.name);
@@ -177,6 +220,9 @@ export const useLayers = () => {
height: img.height,
x: 0,
y: 0,
rotation: 0,
flipX: false,
flipY: false,
};
l.sprites = [...l.sprites, next];
};
@@ -233,6 +279,9 @@ export const useLayers = () => {
updateSpriteInLayer,
updateSpriteCell,
removeSprite,
removeSprites,
rotateSprite,
flipSprite,
replaceSprite,
addSprite,
processImageFiles,

View File

@@ -48,7 +48,16 @@ export const shareSpritesheet = async (layersRef: Ref<Layer[]>, columns: Ref<num
if (!ctx) return null;
canvas.width = sprite.width;
canvas.height = sprite.height;
ctx.drawImage(sprite.img, 0, 0);
if (sprite.rotation || sprite.flipX || sprite.flipY) {
ctx.save();
ctx.translate(sprite.width / 2, sprite.height / 2);
ctx.rotate((sprite.rotation * Math.PI) / 180);
ctx.scale(sprite.flipX ? -1 : 1, sprite.flipY ? -1 : 1);
ctx.drawImage(sprite.img, -sprite.width / 2, -sprite.height / 2);
ctx.restore();
} else {
ctx.drawImage(sprite.img, 0, 0);
}
const base64 = canvas.toDataURL('image/png');
return {
id: sprite.id,
@@ -56,6 +65,9 @@ export const shareSpritesheet = async (layersRef: Ref<Layer[]>, columns: Ref<num
height: sprite.height,
x: sprite.x,
y: sprite.y,
rotation: sprite.rotation || 0,
flipX: sprite.flipX || false,
flipY: sprite.flipY || false,
base64,
name: sprite.file.name,
};