669 lines
21 KiB
TypeScript
669 lines
21 KiB
TypeScript
import { ref, reactive, computed } from 'vue';
|
|
|
|
export interface Sprite {
|
|
img: HTMLImageElement;
|
|
width: number;
|
|
height: number;
|
|
x: number;
|
|
y: number;
|
|
name: string;
|
|
id: string;
|
|
uploadOrder: number;
|
|
}
|
|
|
|
export interface CellSize {
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export interface AnimationState {
|
|
canvas: HTMLCanvasElement | null;
|
|
ctx: CanvasRenderingContext2D | null;
|
|
currentFrame: number;
|
|
isPlaying: boolean;
|
|
frameRate: number;
|
|
lastFrameTime: number;
|
|
animationId: number | null;
|
|
slider: HTMLInputElement | null;
|
|
manualUpdate: boolean;
|
|
}
|
|
|
|
const sprites = ref<Sprite[]>([]);
|
|
const canvas = ref<HTMLCanvasElement | null>(null);
|
|
const ctx = ref<CanvasRenderingContext2D | null>(null);
|
|
const cellSize = reactive<CellSize>({ width: 0, height: 0 });
|
|
const columns = ref(4); // Default number of columns
|
|
const draggedSprite = ref<Sprite | null>(null);
|
|
const dragOffset = reactive({ x: 0, y: 0 });
|
|
const isShiftPressed = ref(false);
|
|
const isModalOpen = ref(false);
|
|
const isSettingsModalOpen = ref(false);
|
|
const isSpritesModalOpen = ref(false);
|
|
const isHelpModalOpen = ref(false);
|
|
const zoomLevel = ref(1); // Default zoom level (1 = 100%)
|
|
|
|
// Preview border settings
|
|
const previewBorder = reactive({
|
|
enabled: false,
|
|
color: '#ff0000', // Default red color
|
|
width: 2, // Default width in pixels
|
|
});
|
|
|
|
export function useSpritesheetStore() {
|
|
const animation = reactive<AnimationState>({
|
|
canvas: null,
|
|
ctx: null,
|
|
currentFrame: 0,
|
|
isPlaying: false,
|
|
frameRate: 10,
|
|
lastFrameTime: 0,
|
|
animationId: null,
|
|
slider: null,
|
|
manualUpdate: false,
|
|
});
|
|
|
|
const notification = reactive({
|
|
isVisible: false,
|
|
message: '',
|
|
type: 'success' as 'success' | 'error',
|
|
});
|
|
|
|
function addSprites(newSprites: Sprite[]) {
|
|
if (newSprites.length === 0) {
|
|
console.warn('Store: Attempted to add empty sprites array');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Validate sprites before adding them
|
|
const validSprites = newSprites.filter(sprite => {
|
|
if (!sprite.img || sprite.width <= 0 || sprite.height <= 0) {
|
|
console.error('Store: Invalid sprite detected', sprite);
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (validSprites.length === 0) {
|
|
console.error('Store: No valid sprites to add');
|
|
return;
|
|
}
|
|
|
|
sprites.value.push(...validSprites);
|
|
sprites.value.sort((a, b) => a.uploadOrder - b.uploadOrder);
|
|
updateCellSize();
|
|
autoArrangeSprites();
|
|
} catch (error) {
|
|
console.error('Store: Error adding sprites:', error);
|
|
}
|
|
}
|
|
|
|
function updateCellSize() {
|
|
if (sprites.value.length === 0) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let maxWidth = 0;
|
|
let maxHeight = 0;
|
|
|
|
sprites.value.forEach(sprite => {
|
|
if (sprite.width <= 0 || sprite.height <= 0) {
|
|
console.warn('Store: Sprite with invalid dimensions detected', sprite);
|
|
return;
|
|
}
|
|
maxWidth = Math.max(maxWidth, sprite.width);
|
|
maxHeight = Math.max(maxHeight, sprite.height);
|
|
});
|
|
|
|
if (maxWidth === 0 || maxHeight === 0) {
|
|
console.error('Store: Failed to calculate valid cell size');
|
|
return;
|
|
}
|
|
|
|
cellSize.width = maxWidth;
|
|
cellSize.height = maxHeight;
|
|
|
|
updateCanvasSize();
|
|
} catch (error) {
|
|
console.error('Store: Error updating cell size:', error);
|
|
}
|
|
}
|
|
|
|
function updateCanvasSize() {
|
|
if (!canvas.value) {
|
|
console.warn('Store: Canvas not available for size update');
|
|
return;
|
|
}
|
|
|
|
if (sprites.value.length === 0) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const totalSprites = sprites.value.length;
|
|
const cols = columns.value;
|
|
const rows = Math.ceil(totalSprites / cols);
|
|
|
|
if (cellSize.width <= 0 || cellSize.height <= 0) {
|
|
console.error('Store: Invalid cell size for canvas update', cellSize);
|
|
return;
|
|
}
|
|
|
|
const newWidth = cols * cellSize.width;
|
|
const newHeight = rows * cellSize.height;
|
|
|
|
// Ensure the canvas is large enough to display all sprites
|
|
if (canvas.value.width !== newWidth || canvas.value.height !== newHeight) {
|
|
canvas.value.width = newWidth;
|
|
canvas.value.height = newHeight;
|
|
|
|
// Emit an event to update the wrapper dimensions
|
|
window.dispatchEvent(
|
|
new CustomEvent('canvas-size-updated', {
|
|
detail: { width: newWidth, height: newHeight },
|
|
})
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error('Store: Error updating canvas size:', error);
|
|
}
|
|
}
|
|
|
|
function autoArrangeSprites() {
|
|
if (sprites.value.length === 0) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (cellSize.width <= 0 || cellSize.height <= 0) {
|
|
console.error('Store: Invalid cell size for auto-arranging', cellSize);
|
|
return;
|
|
}
|
|
|
|
// First update the canvas size to ensure it's large enough
|
|
updateCanvasSize();
|
|
|
|
// Then position each sprite in its grid cell
|
|
sprites.value.forEach((sprite, index) => {
|
|
const column = index % columns.value;
|
|
const row = Math.floor(index / columns.value);
|
|
|
|
sprite.x = column * cellSize.width;
|
|
sprite.y = row * cellSize.height;
|
|
});
|
|
|
|
// Check if the canvas is ready before attempting to render
|
|
if (!ctx.value || !canvas.value) {
|
|
console.warn('Store: Canvas or context not available for rendering after auto-arrange');
|
|
return;
|
|
}
|
|
|
|
renderSpritesheetPreview();
|
|
|
|
if (!animation.isPlaying && animation.manualUpdate && isModalOpen.value) {
|
|
renderAnimationFrame(animation.currentFrame);
|
|
}
|
|
} catch (error) {
|
|
console.error('Store: Error auto-arranging sprites:', error);
|
|
}
|
|
}
|
|
|
|
function renderSpritesheetPreview(showGrid = true) {
|
|
if (!ctx.value || !canvas.value) {
|
|
console.error('Store: Canvas or context not available for rendering, will retry when ready');
|
|
setTimeout(() => {
|
|
if (ctx.value && canvas.value) {
|
|
renderSpritesheetPreview(showGrid);
|
|
}
|
|
}, 100);
|
|
return;
|
|
}
|
|
|
|
if (sprites.value.length === 0) return;
|
|
|
|
try {
|
|
// Make sure the canvas size is correct before rendering
|
|
updateCanvasSize();
|
|
|
|
// Clear the canvas
|
|
ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
|
|
|
if (showGrid) {
|
|
drawGrid();
|
|
}
|
|
|
|
// First, collect all occupied cells
|
|
const occupiedCells = new Set<string>();
|
|
sprites.value.forEach(sprite => {
|
|
const cellX = Math.floor(sprite.x / cellSize.width);
|
|
const cellY = Math.floor(sprite.y / cellSize.height);
|
|
occupiedCells.add(`${cellX},${cellY}`);
|
|
});
|
|
|
|
// Draw each sprite - remove the zoom scaling from context
|
|
sprites.value.forEach((sprite, index) => {
|
|
try {
|
|
if (!sprite.img) {
|
|
console.warn(`Store: Sprite at index ${index} has no image, skipping render`);
|
|
return;
|
|
}
|
|
|
|
// Check if sprite is within canvas bounds
|
|
if (sprite.x >= 0 && sprite.y >= 0 && sprite.x + sprite.width <= canvas.value!.width && sprite.y + sprite.height <= canvas.value!.height) {
|
|
if (sprite.img.complete && sprite.img.naturalWidth !== 0) {
|
|
// For pixel art, ensure we're drawing at exact pixel boundaries
|
|
const x = Math.round(sprite.x);
|
|
const y = Math.round(sprite.y);
|
|
|
|
// Get the frame-specific offset for this sprite
|
|
const frameOffset = getSpriteOffset(index);
|
|
|
|
// Apply the frame-specific offset to the sprite position
|
|
const finalX = x + frameOffset.x;
|
|
const finalY = y + frameOffset.y;
|
|
|
|
// Draw the image at its final position with pixel-perfect rendering
|
|
ctx.value!.imageSmoothingEnabled = false; // Keep pixel art sharp
|
|
ctx.value!.drawImage(sprite.img, finalX, finalY, sprite.width, sprite.height);
|
|
} else {
|
|
console.warn(`Store: Sprite image ${index} not fully loaded, setting onload handler`);
|
|
sprite.img.onload = () => {
|
|
if (ctx.value && canvas.value) {
|
|
// For pixel art, ensure we're drawing at exact pixel boundaries
|
|
const x = Math.round(sprite.x);
|
|
const y = Math.round(sprite.y);
|
|
|
|
// Get the frame-specific offset for this sprite
|
|
const frameOffset = getSpriteOffset(index);
|
|
|
|
// Apply the frame-specific offset to the sprite position
|
|
const finalX = x + frameOffset.x;
|
|
const finalY = y + frameOffset.y;
|
|
|
|
ctx.value.imageSmoothingEnabled = false; // Keep pixel art sharp
|
|
ctx.value.drawImage(sprite.img, finalX, finalY, sprite.width, sprite.height);
|
|
}
|
|
};
|
|
}
|
|
} else {
|
|
console.warn(`Store: Sprite at index ${index} is outside canvas bounds: sprite(${sprite.x},${sprite.y}) canvas(${canvas.value!.width},${canvas.value!.height})`);
|
|
}
|
|
} catch (spriteError) {
|
|
console.error(`Store: Error rendering sprite at index ${index}:`, spriteError);
|
|
}
|
|
});
|
|
|
|
// Draw borders around occupied cells if enabled (preview only)
|
|
if (previewBorder.enabled && occupiedCells.size > 0) {
|
|
ctx.value!.strokeStyle = previewBorder.color;
|
|
ctx.value!.lineWidth = previewBorder.width / zoomLevel.value; // Adjust for zoom
|
|
|
|
// Draw borders around each occupied cell
|
|
occupiedCells.forEach(cellKey => {
|
|
const [cellX, cellY] = cellKey.split(',').map(Number);
|
|
|
|
// Calculate pixel-perfect coordinates for the cell
|
|
// Add 0.5 to align with pixel boundaries for crisp lines
|
|
const x = Math.floor(cellX * cellSize.width) + 0.5;
|
|
const y = Math.floor(cellY * cellSize.height) + 0.5;
|
|
|
|
// Adjust width and height to ensure the border is inside the cell
|
|
const width = cellSize.width - 1;
|
|
const height = cellSize.height - 1;
|
|
|
|
ctx.value!.strokeRect(x, y, width, height);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Store: Error in renderSpritesheetPreview:', error);
|
|
}
|
|
}
|
|
|
|
function applyOffsetsToMainView() {
|
|
sprites.value.forEach((sprite, index) => {
|
|
const frameOffset = getSpriteOffset(index);
|
|
if (frameOffset.x !== 0 || frameOffset.y !== 0) {
|
|
// Update the sprite's position to include the offset
|
|
sprite.x += frameOffset.x;
|
|
sprite.y += frameOffset.y;
|
|
|
|
// Reset the offset
|
|
frameOffset.x = 0;
|
|
frameOffset.y = 0;
|
|
}
|
|
});
|
|
|
|
// Reset current offset
|
|
currentSpriteOffset.x = 0;
|
|
currentSpriteOffset.y = 0;
|
|
|
|
// Re-render the main view
|
|
renderSpritesheetPreview();
|
|
}
|
|
|
|
function drawGrid() {
|
|
if (!ctx.value || !canvas.value) return;
|
|
|
|
ctx.value.strokeStyle = '#333';
|
|
ctx.value.lineWidth = 1 / zoomLevel.value; // Adjust line width based on zoom level
|
|
|
|
// Calculate the visible area based on zoom level
|
|
const visibleWidth = canvas.value.width / zoomLevel.value;
|
|
const visibleHeight = canvas.value.height / zoomLevel.value;
|
|
|
|
// Draw vertical lines - ensure pixel-perfect grid lines
|
|
for (let x = 0; x <= visibleWidth; x += cellSize.width) {
|
|
const pixelX = Math.floor(x) + 0.5; // Align to pixel boundary for crisp lines
|
|
ctx.value.beginPath();
|
|
ctx.value.moveTo(pixelX, 0);
|
|
ctx.value.lineTo(pixelX, visibleHeight);
|
|
ctx.value.stroke();
|
|
}
|
|
|
|
// Draw horizontal lines - ensure pixel-perfect grid lines
|
|
for (let y = 0; y <= visibleHeight; y += cellSize.height) {
|
|
const pixelY = Math.floor(y) + 0.5; // Align to pixel boundary for crisp lines
|
|
ctx.value.beginPath();
|
|
ctx.value.moveTo(0, pixelY);
|
|
ctx.value.lineTo(visibleWidth, pixelY);
|
|
ctx.value.stroke();
|
|
}
|
|
}
|
|
|
|
function highlightSprite(spriteId: string) {
|
|
if (!ctx.value || !canvas.value) return;
|
|
|
|
const sprite = sprites.value.find(s => s.id === spriteId);
|
|
if (!sprite) return;
|
|
|
|
// Calculate the cell coordinates
|
|
const cellX = Math.floor(sprite.x / cellSize.width);
|
|
const cellY = Math.floor(sprite.y / cellSize.height);
|
|
|
|
// Briefly flash the cell
|
|
ctx.value.save();
|
|
ctx.value.fillStyle = 'rgba(0, 150, 255, 0.3)';
|
|
ctx.value.fillRect(cellX * cellSize.width, cellY * cellSize.height, cellSize.width, cellSize.height);
|
|
ctx.value.restore();
|
|
|
|
// Reset after a short delay
|
|
setTimeout(() => {
|
|
renderSpritesheetPreview();
|
|
}, 500);
|
|
}
|
|
|
|
function clearAllSprites() {
|
|
if (!canvas.value || !ctx.value) return;
|
|
|
|
sprites.value = [];
|
|
canvas.value.width = 400;
|
|
canvas.value.height = 300;
|
|
ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
|
|
|
if (animation.canvas && animation.ctx) {
|
|
animation.canvas.width = 200;
|
|
animation.canvas.height = 200;
|
|
animation.ctx.clearRect(0, 0, animation.canvas.width, animation.canvas.height);
|
|
}
|
|
|
|
animation.currentFrame = 0;
|
|
isModalOpen.value = false;
|
|
}
|
|
|
|
function downloadSpritesheet() {
|
|
if (sprites.value.length === 0 || !canvas.value) {
|
|
showNotification('No sprites to download', 'error');
|
|
return;
|
|
}
|
|
|
|
const tempCanvas = document.createElement('canvas');
|
|
tempCanvas.width = canvas.value.width;
|
|
tempCanvas.height = canvas.value.height;
|
|
const tempCtx = tempCanvas.getContext('2d');
|
|
|
|
if (!tempCtx) {
|
|
showNotification('Failed to create download context', 'error');
|
|
return;
|
|
}
|
|
|
|
tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
|
|
|
|
// Ensure pixel art remains sharp in the downloaded file
|
|
tempCtx.imageSmoothingEnabled = false;
|
|
|
|
sprites.value.forEach(sprite => {
|
|
// Use rounded coordinates for pixel-perfect rendering
|
|
const x = Math.round(sprite.x);
|
|
const y = Math.round(sprite.y);
|
|
tempCtx.drawImage(sprite.img, x, y);
|
|
});
|
|
|
|
const link = document.createElement('a');
|
|
link.download = 'spritesheet.png';
|
|
link.href = tempCanvas.toDataURL('image/png');
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
showNotification('Spritesheet downloaded successfully');
|
|
}
|
|
|
|
function startAnimation() {
|
|
if (sprites.value.length === 0 || !animation.canvas) return;
|
|
|
|
animation.isPlaying = true;
|
|
animation.lastFrameTime = performance.now();
|
|
animation.manualUpdate = false;
|
|
|
|
animation.canvas.width = cellSize.width;
|
|
animation.canvas.height = cellSize.height;
|
|
|
|
// Start the animation loop without resetting sprite offset
|
|
animationLoop();
|
|
}
|
|
|
|
function stopAnimation() {
|
|
animation.isPlaying = false;
|
|
|
|
if (animation.animationId) {
|
|
cancelAnimationFrame(animation.animationId);
|
|
animation.animationId = null;
|
|
}
|
|
}
|
|
|
|
function renderAnimationFrame(frameIndex: number, showAllSprites = false, spriteOffset = { x: 0, y: 0 }) {
|
|
if (sprites.value.length === 0 || !animation.canvas || !animation.ctx) return;
|
|
|
|
if (animation.canvas.width !== cellSize.width || animation.canvas.height !== cellSize.height) {
|
|
animation.canvas.width = cellSize.width;
|
|
animation.canvas.height = cellSize.height;
|
|
}
|
|
|
|
animation.ctx.clearRect(0, 0, animation.canvas.width, animation.canvas.height);
|
|
|
|
// Draw background (transparent by default)
|
|
animation.ctx.fillStyle = 'transparent';
|
|
animation.ctx.fillRect(0, 0, animation.canvas.width, animation.canvas.height);
|
|
|
|
// Keep pixel art sharp
|
|
animation.ctx.imageSmoothingEnabled = false;
|
|
|
|
// If showAllSprites is enabled, draw all sprites with transparency
|
|
if (showAllSprites && sprites.value.length > 1) {
|
|
// Save the current context state
|
|
animation.ctx.save();
|
|
|
|
// Set global alpha for background sprites
|
|
animation.ctx.globalAlpha = 0.3;
|
|
|
|
// Draw all sprites except the current one
|
|
sprites.value.forEach((sprite, index) => {
|
|
if (index !== frameIndex) {
|
|
const spriteCellX = Math.floor(sprite.x / cellSize.width);
|
|
const spriteCellY = Math.floor(sprite.y / cellSize.height);
|
|
|
|
// Calculate precise offset for pixel-perfect rendering
|
|
const spriteOffsetX = Math.round(sprite.x - spriteCellX * cellSize.width);
|
|
const spriteOffsetY = Math.round(sprite.y - spriteCellY * cellSize.height);
|
|
|
|
// Draw the sprite with transparency
|
|
animation.ctx.drawImage(sprite.img, spriteOffsetX, spriteOffsetY);
|
|
}
|
|
});
|
|
|
|
// Restore the context to full opacity
|
|
animation.ctx.restore();
|
|
}
|
|
|
|
// Get the current sprite
|
|
const currentSprite = sprites.value[frameIndex % sprites.value.length];
|
|
|
|
const cellX = Math.floor(currentSprite.x / cellSize.width);
|
|
const cellY = Math.floor(currentSprite.y / cellSize.height);
|
|
|
|
// Calculate the original position (without user offset)
|
|
const originalOffsetX = Math.round(currentSprite.x - cellX * cellSize.width);
|
|
const originalOffsetY = Math.round(currentSprite.y - cellY * cellSize.height);
|
|
|
|
// Calculate precise offset for pixel-perfect rendering, including the user's drag offset
|
|
const offsetX = originalOffsetX + spriteOffset.x;
|
|
const offsetY = originalOffsetY + spriteOffset.y;
|
|
|
|
// Draw the current sprite at full opacity at the new position
|
|
animation.ctx.drawImage(currentSprite.img, offsetX, offsetY);
|
|
|
|
// Draw border around the cell if enabled (only for preview, not included in download)
|
|
if (previewBorder.enabled) {
|
|
animation.ctx.strokeStyle = previewBorder.color;
|
|
animation.ctx.lineWidth = previewBorder.width;
|
|
|
|
// Use pixel-perfect coordinates for the border (0.5 offset for crisp lines)
|
|
const x = 0.5;
|
|
const y = 0.5;
|
|
const width = animation.canvas.width - 1;
|
|
const height = animation.canvas.height - 1;
|
|
|
|
animation.ctx.strokeRect(x, y, width, height);
|
|
}
|
|
}
|
|
|
|
// Store the current sprite offset for animation playback
|
|
// We'll use a Map to store offsets for each frame, so they're preserved when switching frames
|
|
const spriteOffsets = reactive(new Map<number, { x: number; y: number }>());
|
|
|
|
// Current sprite offset is a reactive object that will be used for the current frame
|
|
const currentSpriteOffset = reactive({ x: 0, y: 0 });
|
|
|
|
// Helper function to get the offset for a specific frame
|
|
function getSpriteOffset(frameIndex: number) {
|
|
if (!spriteOffsets.has(frameIndex)) {
|
|
spriteOffsets.set(frameIndex, { x: 0, y: 0 });
|
|
}
|
|
return spriteOffsets.get(frameIndex)!;
|
|
}
|
|
|
|
function animationLoop(timestamp?: number) {
|
|
if (!animation.isPlaying) return;
|
|
|
|
const currentTime = timestamp || performance.now();
|
|
const elapsed = currentTime - animation.lastFrameTime;
|
|
const frameInterval = 1000 / animation.frameRate;
|
|
|
|
if (elapsed >= frameInterval) {
|
|
animation.lastFrameTime = currentTime;
|
|
|
|
if (sprites.value.length > 0) {
|
|
// Get the stored offset for the current frame
|
|
const frameOffset = getSpriteOffset(animation.currentFrame);
|
|
|
|
// Update the current offset for rendering
|
|
currentSpriteOffset.x = frameOffset.x;
|
|
currentSpriteOffset.y = frameOffset.y;
|
|
|
|
// Render the current frame with its offset
|
|
renderAnimationFrame(animation.currentFrame, false, frameOffset);
|
|
|
|
// Move to the next frame
|
|
animation.currentFrame = (animation.currentFrame + 1) % sprites.value.length;
|
|
|
|
if (animation.slider) {
|
|
animation.slider.value = animation.currentFrame.toString();
|
|
}
|
|
}
|
|
}
|
|
|
|
animation.animationId = requestAnimationFrame(animationLoop);
|
|
}
|
|
|
|
function showNotification(message: string, type: 'success' | 'error' = 'success') {
|
|
notification.message = message;
|
|
notification.type = type;
|
|
notification.isVisible = true;
|
|
|
|
setTimeout(() => {
|
|
notification.isVisible = false;
|
|
}, 3000);
|
|
}
|
|
|
|
function zoomIn() {
|
|
// Increase zoom level by 0.1, max 3.0 (300%)
|
|
zoomLevel.value = Math.min(3.0, zoomLevel.value + 0.1);
|
|
renderSpritesheetPreview();
|
|
showNotification(`Zoom: ${Math.round(zoomLevel.value * 100)}%`);
|
|
}
|
|
|
|
function zoomOut() {
|
|
// Decrease zoom level by 0.1, min 0.5 (50%)
|
|
zoomLevel.value = Math.max(0.5, zoomLevel.value - 0.1);
|
|
renderSpritesheetPreview();
|
|
showNotification(`Zoom: ${Math.round(zoomLevel.value * 100)}%`);
|
|
}
|
|
|
|
function resetZoom() {
|
|
// Reset to default zoom level (100%)
|
|
zoomLevel.value = 1;
|
|
renderSpritesheetPreview();
|
|
showNotification('Zoom reset to 100%');
|
|
}
|
|
|
|
return {
|
|
sprites,
|
|
canvas,
|
|
ctx,
|
|
cellSize,
|
|
columns,
|
|
draggedSprite,
|
|
dragOffset,
|
|
isShiftPressed,
|
|
isModalOpen,
|
|
isSettingsModalOpen,
|
|
isSpritesModalOpen,
|
|
isHelpModalOpen,
|
|
animation,
|
|
notification,
|
|
zoomLevel,
|
|
previewBorder,
|
|
currentSpriteOffset,
|
|
spriteOffsets,
|
|
getSpriteOffset,
|
|
addSprites,
|
|
updateCellSize,
|
|
updateCanvasSize,
|
|
autoArrangeSprites,
|
|
renderSpritesheetPreview,
|
|
drawGrid,
|
|
highlightSprite,
|
|
clearAllSprites,
|
|
downloadSpritesheet,
|
|
startAnimation,
|
|
stopAnimation,
|
|
renderAnimationFrame,
|
|
showNotification,
|
|
zoomIn,
|
|
zoomOut,
|
|
resetZoom,
|
|
applyOffsetsToMainView,
|
|
};
|
|
}
|