v1.3.0
This commit is contained in:
parent
702d4f38ea
commit
b884487ec9
@ -1,5 +1,10 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [1.3.0] - 2025-04-06
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- 📄 When importing a spritesheet, the tool will now remove transparent from the edges of each sprite so you can move them inside their cells.
|
||||||
|
|
||||||
## [1.2.0] - 2025-04-06
|
## [1.2.0] - 2025-04-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
36
src/App.vue
36
src/App.vue
@ -98,6 +98,14 @@
|
|||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SpriteFile {
|
||||||
|
file: File;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
const sprites = ref<Sprite[]>([]);
|
const sprites = ref<Sprite[]>([]);
|
||||||
const columns = ref(4);
|
const columns = ref(4);
|
||||||
const isPreviewModalOpen = ref(false);
|
const isPreviewModalOpen = ref(false);
|
||||||
@ -261,9 +269,31 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle the split spritesheet result
|
// Handle the split spritesheet result
|
||||||
const handleSplitSpritesheet = (files: File[]) => {
|
const handleSplitSpritesheet = (spriteFiles: SpriteFile[]) => {
|
||||||
// Process the split sprite files
|
// Process sprite files with their positions
|
||||||
processImageFiles(files);
|
Promise.all(
|
||||||
|
spriteFiles.map(spriteFile => {
|
||||||
|
return new Promise<Sprite>(resolve => {
|
||||||
|
const url = URL.createObjectURL(spriteFile.file);
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
resolve({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
file: spriteFile.file,
|
||||||
|
img,
|
||||||
|
url,
|
||||||
|
width: img.width,
|
||||||
|
height: img.height,
|
||||||
|
x: spriteFile.x, // Use the position from the splitter
|
||||||
|
y: spriteFile.y, // Use the position from the splitter
|
||||||
|
});
|
||||||
|
};
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
).then(newSprites => {
|
||||||
|
sprites.value = [...sprites.value, ...newSprites];
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export spritesheet as JSON with base64 images
|
// Export spritesheet as JSON with base64 images
|
||||||
|
@ -84,9 +84,17 @@
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: 'close'): void;
|
||||||
(e: 'split', sprites: File[]): void;
|
(e: 'split', sprites: SpriteFile[]): void; // Change from File[] to SpriteFile[]
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
interface SpriteFile {
|
||||||
|
file: File;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Get settings from store
|
// Get settings from store
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
@ -155,6 +163,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSpriteBoundingBox(ctx: CanvasRenderingContext2D, width: number, height: number) {
|
||||||
|
const imageData = ctx.getImageData(0, 0, width, height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
let minX = width;
|
||||||
|
let minY = height;
|
||||||
|
let maxX = 0;
|
||||||
|
let maxY = 0;
|
||||||
|
let hasContent = false;
|
||||||
|
|
||||||
|
// Scan through all pixels to find the bounding box
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const idx = (y * width + x) * 4;
|
||||||
|
// Check if pixel is not transparent (alpha > 0)
|
||||||
|
if (data[idx + 3] > 10) {
|
||||||
|
// Allow some tolerance for compression artifacts
|
||||||
|
minX = Math.min(minX, x);
|
||||||
|
minY = Math.min(minY, y);
|
||||||
|
maxX = Math.max(maxX, x);
|
||||||
|
maxY = Math.max(maxY, y);
|
||||||
|
hasContent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no non-transparent pixels found, return null
|
||||||
|
if (!hasContent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return bounding box with a small padding
|
||||||
|
return {
|
||||||
|
x: Math.max(0, minX - 1),
|
||||||
|
y: Math.max(0, minY - 1),
|
||||||
|
width: Math.min(width, maxX - minX + 3), // +1 for inclusive bounds, +2 for padding
|
||||||
|
height: Math.min(height, maxY - minY + 3),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Split spritesheet manually based on rows and columns
|
// Split spritesheet manually based on rows and columns
|
||||||
async function splitSpritesheet(img: HTMLImageElement, rows: number, columns: number) {
|
async function splitSpritesheet(img: HTMLImageElement, rows: number, columns: number) {
|
||||||
const spriteWidth = Math.floor(img.width / columns);
|
const spriteWidth = Math.floor(img.width / columns);
|
||||||
@ -162,10 +210,15 @@
|
|||||||
|
|
||||||
const sprites: SpritePreview[] = [];
|
const sprites: SpritePreview[] = [];
|
||||||
|
|
||||||
// Create a canvas for processing
|
// Create a canvas for processing the full sprite
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) return;
|
|
||||||
|
// Create a second canvas for the cropped sprite
|
||||||
|
const croppedCanvas = document.createElement('canvas');
|
||||||
|
const croppedCtx = croppedCanvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!ctx || !croppedCtx) return;
|
||||||
|
|
||||||
canvas.width = spriteWidth;
|
canvas.width = spriteWidth;
|
||||||
canvas.height = spriteHeight;
|
canvas.height = spriteHeight;
|
||||||
@ -184,15 +237,43 @@
|
|||||||
|
|
||||||
// If we're not removing empty sprites or the sprite is not empty
|
// If we're not removing empty sprites or the sprite is not empty
|
||||||
if (!removeEmpty.value || !isEmpty) {
|
if (!removeEmpty.value || !isEmpty) {
|
||||||
// Convert to data URL
|
// Get bounding box of non-transparent pixels
|
||||||
const url = canvas.toDataURL('image/png');
|
const boundingBox = getSpriteBoundingBox(ctx, spriteWidth, spriteHeight);
|
||||||
|
|
||||||
|
let url;
|
||||||
|
let x = 0; // Default position (will be updated if we have a bounding box)
|
||||||
|
let y = 0;
|
||||||
|
let width = spriteWidth;
|
||||||
|
let height = spriteHeight;
|
||||||
|
|
||||||
|
if (boundingBox) {
|
||||||
|
// The key change: preserve the original position where the sprite was found
|
||||||
|
x = boundingBox.x;
|
||||||
|
y = boundingBox.y;
|
||||||
|
width = boundingBox.width;
|
||||||
|
height = boundingBox.height;
|
||||||
|
|
||||||
|
// Set dimensions for the cropped sprite
|
||||||
|
croppedCanvas.width = boundingBox.width;
|
||||||
|
croppedCanvas.height = boundingBox.height;
|
||||||
|
|
||||||
|
// Draw only the non-transparent part
|
||||||
|
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
|
||||||
|
croppedCtx.drawImage(canvas, boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height, 0, 0, boundingBox.width, boundingBox.height);
|
||||||
|
|
||||||
|
// Convert to data URL
|
||||||
|
url = croppedCanvas.toDataURL('image/png');
|
||||||
|
} else {
|
||||||
|
// No non-transparent pixels found, use the original sprite
|
||||||
|
url = canvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
|
|
||||||
sprites.push({
|
sprites.push({
|
||||||
url,
|
url,
|
||||||
x: 0,
|
x,
|
||||||
y: 0,
|
y,
|
||||||
width: spriteWidth,
|
width,
|
||||||
height: spriteHeight,
|
height,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -298,8 +379,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert preview sprites to actual files
|
// Convert preview sprites to actual files
|
||||||
async function createSpriteFiles(): Promise<File[]> {
|
async function createSpriteFiles(): Promise<SpriteFile[]> {
|
||||||
const files: File[] = [];
|
const spriteFiles: SpriteFile[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < previewSprites.value.length; i++) {
|
for (let i = 0; i < previewSprites.value.length; i++) {
|
||||||
const sprite = previewSprites.value[i];
|
const sprite = previewSprites.value[i];
|
||||||
@ -312,10 +393,17 @@
|
|||||||
const fileName = `sprite_${i + 1}.png`;
|
const fileName = `sprite_${i + 1}.png`;
|
||||||
const file = new File([blob], fileName, { type: 'image/png' });
|
const file = new File([blob], fileName, { type: 'image/png' });
|
||||||
|
|
||||||
files.push(file);
|
// Create sprite file with position information
|
||||||
|
spriteFiles.push({
|
||||||
|
file,
|
||||||
|
x: sprite.x,
|
||||||
|
y: sprite.y,
|
||||||
|
width: sprite.width,
|
||||||
|
height: sprite.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
return spriteFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
Loading…
x
Reference in New Issue
Block a user