Files
spritesheet-generator/src/composables/useShare.ts
2026-01-01 18:23:42 +01:00

170 lines
4.9 KiB
TypeScript

import type { Ref } from 'vue';
import type { Layer } from '@/types/sprites';
const POCKETBASE_URL = import.meta.env.VITE_POCKETBASE_URL;
const COLLECTION = 'spritesheets';
export interface SpritesheetConfig {
version: number;
columns: number;
negativeSpacingEnabled: boolean;
backgroundColor: string;
manualCellSizeEnabled: boolean;
manualCellWidth: number;
manualCellHeight: number;
}
export interface SharedSprite {
id: string;
width: number;
height: number;
x: number;
y: number;
rotation: number;
flipX: boolean;
flipY: boolean;
base64: string;
name?: string;
}
export interface SharedLayer {
id: string;
name: string;
visible: boolean;
locked: boolean;
sprites: SharedSprite[];
}
export interface SpritesheetRecord {
id: string;
config: SpritesheetConfig;
sprites: SharedLayer[];
created: string;
updated: string;
}
export interface ShareResult {
id: string;
url: string;
}
/**
* Build the shareable URL for a spritesheet
*/
export const buildShareUrl = (id: string): string => {
return `${window.location.origin}/share/${id}`;
};
/**
* Share a spritesheet by uploading to PocketBase
*/
export const shareSpritesheet = async (layersRef: Ref<Layer[]>, columns: Ref<number>, negativeSpacingEnabled: Ref<boolean>, backgroundColor?: Ref<string>, manualCellSizeEnabled?: Ref<boolean>, manualCellWidth?: Ref<number>, manualCellHeight?: Ref<number>): Promise<ShareResult> => {
// Build layers data with base64 sprites (same format as exportSpritesheetJSON)
const layersData = await Promise.all(
layersRef.value.map(async layer => {
const sprites = await Promise.all(
layer.sprites.map(async sprite => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return null;
canvas.width = sprite.width;
canvas.height = sprite.height;
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');
// Since we bake transformations into the image, set them to 0/false in metadata
return {
id: sprite.id,
width: sprite.width,
height: sprite.height,
x: sprite.x,
y: sprite.y,
rotation: 0,
flipX: false,
flipY: false,
base64,
name: sprite.file.name,
};
})
);
return {
id: layer.id,
name: layer.name,
visible: layer.visible,
locked: layer.locked,
sprites: sprites.filter(Boolean),
};
})
);
const config: SpritesheetConfig = {
version: 2,
columns: columns.value,
negativeSpacingEnabled: negativeSpacingEnabled.value,
backgroundColor: backgroundColor?.value || 'transparent',
manualCellSizeEnabled: manualCellSizeEnabled?.value || false,
manualCellWidth: manualCellWidth?.value || 64,
manualCellHeight: manualCellHeight?.value || 64,
};
const response = await fetch(`${POCKETBASE_URL}/api/collections/${COLLECTION}/records`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
config,
sprites: layersData,
}),
});
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(text || `Request failed with status ${response.status}`);
}
const record = await response.json();
return {
id: record.id,
url: buildShareUrl(record.id),
};
};
/**
* Fetch a shared spritesheet from PocketBase
*/
export const fetchSpritesheet = async (id: string): Promise<SpritesheetRecord> => {
const response = await fetch(`${POCKETBASE_URL}/api/collections/${COLLECTION}/records/${id}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error('Spritesheet not found');
}
const text = await response.text().catch(() => '');
throw new Error(text || `Request failed with status ${response.status}`);
}
return response.json();
};
/**
* Composable hook for share functionality
*/
export const useShare = (layersRef: Ref<Layer[]>, columns: Ref<number>, negativeSpacingEnabled: Ref<boolean>, backgroundColor?: Ref<string>, manualCellSizeEnabled?: Ref<boolean>, manualCellWidth?: Ref<number>, manualCellHeight?: Ref<number>) => {
const share = () => shareSpritesheet(layersRef, columns, negativeSpacingEnabled, backgroundColor, manualCellSizeEnabled, manualCellWidth, manualCellHeight);
return {
share,
fetchSpritesheet,
buildShareUrl,
};
};