Visible Frames
@@ -513,4 +513,36 @@
cursor: pointer;
border: none;
}
+
+ /* Custom scrollbar for controls panel */
+ .custom-scrollbar::-webkit-scrollbar,
+ .lg\:overflow-y-auto::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ .custom-scrollbar::-webkit-scrollbar-track,
+ .lg\:overflow-y-auto::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ .custom-scrollbar::-webkit-scrollbar-thumb,
+ .lg\:overflow-y-auto::-webkit-scrollbar-thumb {
+ background-color: rgba(156, 163, 175, 0.5);
+ border-radius: 3px;
+ }
+
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover,
+ .lg\:overflow-y-auto::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(156, 163, 175, 0.8);
+ }
+
+ :global(.dark) .custom-scrollbar::-webkit-scrollbar-thumb,
+ :global(.dark) .lg\:overflow-y-auto::-webkit-scrollbar-thumb {
+ background-color: rgba(75, 85, 99, 0.5);
+ }
+
+ :global(.dark) .custom-scrollbar::-webkit-scrollbar-thumb:hover,
+ :global(.dark) .lg\:overflow-y-auto::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(75, 85, 99, 0.8);
+ }
diff --git a/src/composables/useShare.ts b/src/composables/useShare.ts
new file mode 100644
index 0000000..5b2c004
--- /dev/null
+++ b/src/composables/useShare.ts
@@ -0,0 +1,135 @@
+import type { Ref } from 'vue';
+import type { Layer } from '@/types/sprites';
+
+const POCKETBASE_URL = 'https://pb1.adhd.sh';
+const COLLECTION = 'spritesheets';
+
+export interface SpritesheetConfig {
+ version: number;
+ columns: number;
+ negativeSpacingEnabled: boolean;
+ backgroundColor: string;
+ manualCellSizeEnabled: boolean;
+ manualCellWidth: number;
+ manualCellHeight: number;
+}
+
+export interface SpritesheetRecord {
+ id: string;
+ config: SpritesheetConfig;
+ sprites: any[];
+ 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, columns: Ref, negativeSpacingEnabled: Ref, backgroundColor?: Ref, manualCellSizeEnabled?: Ref, manualCellWidth?: Ref, manualCellHeight?: Ref): Promise => {
+ // 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;
+ 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: 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 => {
+ 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, columns: Ref, negativeSpacingEnabled: Ref, backgroundColor?: Ref, manualCellSizeEnabled?: Ref, manualCellWidth?: Ref, manualCellHeight?: Ref) => {
+ const share = () => shareSpritesheet(layersRef, columns, negativeSpacingEnabled, backgroundColor, manualCellSizeEnabled, manualCellWidth, manualCellHeight);
+
+ return {
+ share,
+ fetchSpritesheet,
+ buildShareUrl,
+ };
+};
diff --git a/src/router/index.ts b/src/router/index.ts
index 5a1447a..704ac15 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -39,6 +39,11 @@ const router = createRouter({
name: 'blog-detail',
component: () => import('../views/BlogDetail.vue'),
},
+ {
+ path: '/share/:id',
+ name: 'share',
+ component: () => import('../views/ShareView.vue'),
+ },
],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index 89088cb..063187b 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -45,9 +45,9 @@
-
+
-
+
@@ -203,42 +203,58 @@
ZIP
+
-
+
-
-
-
-
-
+
+
-
@@ -247,6 +263,7 @@
+
@@ -257,7 +274,9 @@
import SpritePreview from '@/components/SpritePreview.vue';
import SpritesheetSplitter from '@/components/SpritesheetSplitter.vue';
import GifFpsModal from '@/components/GifFpsModal.vue';
+ import ShareModal from '@/components/ShareModal.vue';
import { useExportLayers } from '@/composables/useExportLayers';
+ import { useShare } from '@/composables/useShare';
import { useLayers } from '@/composables/useLayers';
import { getMaxDimensionsAcrossLayers } from '@/composables/useLayers';
import { useSettingsStore } from '@/stores/useSettingsStore';
@@ -299,6 +318,7 @@
const activeTab = ref<'canvas' | 'preview'>('canvas');
const isSpritesheetSplitterOpen = ref(false);
const isGifFpsModalOpen = ref(false);
+ const isShareModalOpen = ref(false);
const uploadInput = ref
(null);
const jsonFileInput = ref(null);
const spritesheetImageUrl = ref('');
@@ -378,6 +398,23 @@
isGifFpsModalOpen.value = false;
};
+ // Share functionality
+ const { share } = useShare(layers, columns, toRef(settingsStore, 'negativeSpacingEnabled'), toRef(settingsStore, 'backgroundColor'), toRef(settingsStore, 'manualCellSizeEnabled'), toRef(settingsStore, 'manualCellWidth'), toRef(settingsStore, 'manualCellHeight'));
+
+ const shareFunction = () => share();
+
+ const openShareModal = () => {
+ if (!visibleLayers.value.some(l => l.sprites.length > 0)) {
+ alert('Please upload or import sprites before sharing.');
+ return;
+ }
+ isShareModalOpen.value = true;
+ };
+
+ const closeShareModal = () => {
+ isShareModalOpen.value = false;
+ };
+
const handleSplitSpritesheet = (spriteFiles: SpriteFile[]) => {
processImageFiles(spriteFiles.map(s => s.file));
};
diff --git a/src/views/ShareView.vue b/src/views/ShareView.vue
new file mode 100644
index 0000000..cfaef44
--- /dev/null
+++ b/src/views/ShareView.vue
@@ -0,0 +1,286 @@
+
+
+
+
+
+
+
Loading shared spritesheet...
+
Please wait while we fetch your spritesheet
+
+
+
+
+
+
+
+
Oops! Something went wrong
+
{{ error }}
+
+
+ Back to Home
+
+
+
+
+
+
+
+
+
+
+
+
Shared {{ formatDate(spritesheetData.created) }}
+
+
+
+
+
+
+
+
+
Columns
+
{{ spritesheetData.config.columns }}
+
+
+
+
+
+
Layers
+
{{ spritesheetData.sprites.length }}
+
+
+
+
+
+
Sprites
+
{{ totalSprites }}
+
+
+
+
+
+
Background
+
{{ spritesheetData.config.backgroundColor }}
+
+
+
+
+
+
+
+ Sprite preview
+
+
+
+
+
+ {{ layerIndex + 1 }}
+
+
{{ layer.name || `Layer ${layerIndex + 1}` }}
+
({{ layer.sprites.length }} sprites)
+
+
+
+
+
+ {{ spriteIndex + 1 }}
+
+
+
+ +{{ layer.sprites.length - 12 }}
+ more
+
+
+
+
+
+
+
+
+
+
+ Edit in a copy
+
+
+
+ Download JSON
+
+
+
+ Create new
+
+
+
+
+
+
+
+
+
+