[FEAT] BG color enhancements
This commit is contained in:
@@ -26,14 +26,14 @@
|
|||||||
<!-- Background color picker -->
|
<!-- Background color picker -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label for="bg-color" class="dark:text-gray-200 text-sm sm:text-base">Background:</label>
|
<label for="bg-color" class="dark:text-gray-200 text-sm sm:text-base">Background:</label>
|
||||||
<select id="bg-color" v-model="settingsStore.backgroundColor" class="px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 dark:text-gray-200 text-sm">
|
<select id="bg-color" v-model="bgSelectValue" class="px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 dark:text-gray-200 text-sm">
|
||||||
<option value="transparent">Transparent</option>
|
<option value="transparent">Transparent</option>
|
||||||
<option value="#ffffff">White</option>
|
<option value="#ffffff">White</option>
|
||||||
<option value="#000000">Black</option>
|
<option value="#000000">Black</option>
|
||||||
<option value="#f9fafb">Light Gray</option>
|
<option value="#f9fafb">Light Gray</option>
|
||||||
<option value="custom">Custom...</option>
|
<option value="custom">Custom...</option>
|
||||||
</select>
|
</select>
|
||||||
<input v-if="settingsStore.backgroundColor === 'custom'" type="color" v-model="customColor" @input="settingsStore.setBackgroundColor(customColor)" class="w-8 h-8 border border-gray-300 dark:border-gray-600 rounded cursor-pointer" />
|
<input v-if="bgSelectValue === 'custom'" type="color" v-model="customColor" @input="settingsStore.setBackgroundColor(customColor)" class="w-8 h-8 border border-gray-300 dark:border-gray-600 rounded cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, onUnmounted, toRef, computed } from 'vue';
|
import { ref, onMounted, watch, onUnmounted, toRef, computed, nextTick } from 'vue';
|
||||||
import { useSettingsStore } from '@/stores/useSettingsStore';
|
import { useSettingsStore } from '@/stores/useSettingsStore';
|
||||||
import type { Sprite } from '@/types/sprites';
|
import type { Sprite } from '@/types/sprites';
|
||||||
import { useCanvas2D } from '@/composables/useCanvas2D';
|
import { useCanvas2D } from '@/composables/useCanvas2D';
|
||||||
@@ -213,6 +213,69 @@
|
|||||||
const fileInput = ref<HTMLInputElement | null>(null);
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
const customColor = ref('#ffffff');
|
const customColor = ref('#ffffff');
|
||||||
|
|
||||||
|
// Background select handling: keep the select stable on "custom"
|
||||||
|
const presetBgColors = ['transparent', '#ffffff', '#000000', '#f9fafb'] as const;
|
||||||
|
const isHexColor = (val: string) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val);
|
||||||
|
|
||||||
|
// Ensure customColor mirrors the current stored custom color (only when needed)
|
||||||
|
if (isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any)) {
|
||||||
|
customColor.value = settingsStore.backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track if user is in custom mode to keep picker visible
|
||||||
|
// Initialize to true if current color is custom
|
||||||
|
const isCustomMode = ref(isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any));
|
||||||
|
|
||||||
|
const bgSelectValue = computed<string>({
|
||||||
|
get() {
|
||||||
|
// If user explicitly entered custom mode, stay in it regardless of color value
|
||||||
|
if (isCustomMode.value) {
|
||||||
|
// Keep color picker synced
|
||||||
|
const val = settingsStore.backgroundColor;
|
||||||
|
if (isHexColor(val)) {
|
||||||
|
customColor.value = val;
|
||||||
|
}
|
||||||
|
return 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
const val = settingsStore.backgroundColor;
|
||||||
|
if (presetBgColors.includes(val as any)) return val;
|
||||||
|
if (isHexColor(val)) {
|
||||||
|
// Keep the color picker open and sync its swatch
|
||||||
|
customColor.value = val;
|
||||||
|
isCustomMode.value = true; // Auto-enable custom mode for non-preset hex colors
|
||||||
|
return 'custom';
|
||||||
|
}
|
||||||
|
// Fallback
|
||||||
|
return 'transparent';
|
||||||
|
},
|
||||||
|
set(v: string) {
|
||||||
|
if (v === 'custom') {
|
||||||
|
isCustomMode.value = true;
|
||||||
|
// Switch UI to custom mode but keep the stored value as a color
|
||||||
|
const fallback = '#ffffff';
|
||||||
|
const current = settingsStore.backgroundColor;
|
||||||
|
const fromStore = isHexColor(current) ? current : null;
|
||||||
|
const fromLocal = isHexColor(customColor.value) ? customColor.value : null;
|
||||||
|
const color = fromStore || fromLocal || fallback;
|
||||||
|
customColor.value = color;
|
||||||
|
settingsStore.setBackgroundColor(color);
|
||||||
|
} else {
|
||||||
|
isCustomMode.value = false;
|
||||||
|
settingsStore.setBackgroundColor(v);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure canvas redraw and UI flush after background changes
|
||||||
|
watch(
|
||||||
|
() => settingsStore.backgroundColor,
|
||||||
|
async () => {
|
||||||
|
await nextTick();
|
||||||
|
requestDraw();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Grid metrics used to position offset labels relative to cell size
|
// Grid metrics used to position offset labels relative to cell size
|
||||||
const gridMetrics = computed(() => calculateMaxDimensions());
|
const gridMetrics = computed(() => calculateMaxDimensions());
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Sprite } from '../types/sprites';
|
|||||||
import { getMaxDimensions } from './useSprites';
|
import { getMaxDimensions } from './useSprites';
|
||||||
import { calculateNegativeSpacing } from './useNegativeSpacing';
|
import { calculateNegativeSpacing } from './useNegativeSpacing';
|
||||||
|
|
||||||
export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negativeSpacingEnabled: Ref<boolean>) => {
|
export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negativeSpacingEnabled: Ref<boolean>, backgroundColor?: Ref<string>) => {
|
||||||
const downloadSpritesheet = () => {
|
const downloadSpritesheet = () => {
|
||||||
if (!sprites.value.length) {
|
if (!sprites.value.length) {
|
||||||
alert('Please upload or import sprites before downloading the spritesheet.');
|
alert('Please upload or import sprites before downloading the spritesheet.');
|
||||||
@@ -27,6 +27,12 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
|
|||||||
canvas.height = cellHeight * rows;
|
canvas.height = cellHeight * rows;
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
|
// Apply background color if not transparent
|
||||||
|
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
||||||
|
ctx.fillStyle = backgroundColor.value;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
sprites.value.forEach((sprite, index) => {
|
sprites.value.forEach((sprite, index) => {
|
||||||
const col = index % columns.value;
|
const col = index % columns.value;
|
||||||
const row = Math.floor(index / columns.value);
|
const row = Math.floor(index / columns.value);
|
||||||
@@ -71,6 +77,7 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
|
|||||||
const jsonData = {
|
const jsonData = {
|
||||||
columns: columns.value,
|
columns: columns.value,
|
||||||
negativeSpacingEnabled: negativeSpacingEnabled.value,
|
negativeSpacingEnabled: negativeSpacingEnabled.value,
|
||||||
|
backgroundColor: backgroundColor?.value || 'transparent',
|
||||||
sprites: spritesData.filter(Boolean),
|
sprites: spritesData.filter(Boolean),
|
||||||
};
|
};
|
||||||
const jsonString = JSON.stringify(jsonData, null, 2);
|
const jsonString = JSON.stringify(jsonData, null, 2);
|
||||||
@@ -91,6 +98,7 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
|
|||||||
|
|
||||||
if (jsonData.columns && typeof jsonData.columns === 'number') columns.value = jsonData.columns;
|
if (jsonData.columns && typeof jsonData.columns === 'number') columns.value = jsonData.columns;
|
||||||
if (typeof jsonData.negativeSpacingEnabled === 'boolean') negativeSpacingEnabled.value = jsonData.negativeSpacingEnabled;
|
if (typeof jsonData.negativeSpacingEnabled === 'boolean') negativeSpacingEnabled.value = jsonData.negativeSpacingEnabled;
|
||||||
|
if (typeof jsonData.backgroundColor === 'string' && backgroundColor) backgroundColor.value = jsonData.backgroundColor;
|
||||||
|
|
||||||
// revoke existing blob urls
|
// revoke existing blob urls
|
||||||
if (sprites.value.length) {
|
if (sprites.value.length) {
|
||||||
@@ -156,6 +164,11 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
|
|||||||
|
|
||||||
sprites.value.forEach(sprite => {
|
sprites.value.forEach(sprite => {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
// Apply background color if not transparent
|
||||||
|
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
||||||
|
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));
|
ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y));
|
||||||
gif.addFrame(ctx, { copy: true, delay: 1000 / fps });
|
gif.addFrame(ctx, { copy: true, delay: 1000 / fps });
|
||||||
});
|
});
|
||||||
@@ -192,6 +205,11 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
|
|||||||
|
|
||||||
sprites.value.forEach((sprite, index) => {
|
sprites.value.forEach((sprite, index) => {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
// Apply background color if not transparent
|
||||||
|
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
||||||
|
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));
|
ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y));
|
||||||
const dataURL = canvas.toDataURL('image/png');
|
const dataURL = canvas.toDataURL('image/png');
|
||||||
const binary = atob(dataURL.split(',')[1]);
|
const binary = atob(dataURL.split(',')[1]);
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
|
|||||||
canvas.height = cellHeight * rows;
|
canvas.height = cellHeight * rows;
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
|
// Apply background color to entire canvas if not transparent
|
||||||
|
if (backgroundColor?.value && backgroundColor.value !== 'transparent') {
|
||||||
|
ctx.fillStyle = backgroundColor.value;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
for (let index = 0; index < maxLen; index++) {
|
for (let index = 0; index < maxLen; index++) {
|
||||||
const col = index % columns.value;
|
const col = index % columns.value;
|
||||||
const row = Math.floor(index / columns.value);
|
const row = Math.floor(index / columns.value);
|
||||||
@@ -57,6 +63,7 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
|
|||||||
if (!cellCtx) return;
|
if (!cellCtx) return;
|
||||||
cellCanvas.width = cellWidth;
|
cellCanvas.width = cellWidth;
|
||||||
cellCanvas.height = cellHeight;
|
cellCanvas.height = cellHeight;
|
||||||
|
cellCtx.imageSmoothingEnabled = false;
|
||||||
drawCompositeCell(cellCtx, index, cellWidth, cellHeight, negativeSpacing);
|
drawCompositeCell(cellCtx, index, cellWidth, cellHeight, negativeSpacing);
|
||||||
ctx.drawImage(cellCanvas, cellX, cellY);
|
ctx.drawImage(cellCanvas, cellX, cellY);
|
||||||
}
|
}
|
||||||
@@ -92,7 +99,13 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const json = { version: 2, columns: columns.value, negativeSpacingEnabled: negativeSpacingEnabled.value, layers: layersData };
|
const json = {
|
||||||
|
version: 2,
|
||||||
|
columns: columns.value,
|
||||||
|
negativeSpacingEnabled: negativeSpacingEnabled.value,
|
||||||
|
backgroundColor: backgroundColor?.value || 'transparent',
|
||||||
|
layers: layersData,
|
||||||
|
};
|
||||||
const jsonString = JSON.stringify(json, null, 2);
|
const jsonString = JSON.stringify(json, null, 2);
|
||||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -126,6 +139,7 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
|
|||||||
|
|
||||||
if (typeof data.columns === 'number') columns.value = data.columns;
|
if (typeof data.columns === 'number') columns.value = data.columns;
|
||||||
if (typeof data.negativeSpacingEnabled === 'boolean') negativeSpacingEnabled.value = data.negativeSpacingEnabled;
|
if (typeof data.negativeSpacingEnabled === 'boolean') negativeSpacingEnabled.value = data.negativeSpacingEnabled;
|
||||||
|
if (typeof data.backgroundColor === 'string' && backgroundColor) backgroundColor.value = data.backgroundColor;
|
||||||
|
|
||||||
if (Array.isArray(data.layers)) {
|
if (Array.isArray(data.layers)) {
|
||||||
const newLayers: Layer[] = [];
|
const newLayers: Layer[] = [];
|
||||||
@@ -244,7 +258,13 @@ export const useExportLayers = (layersRef: Ref<Layer[]>, columns: Ref<number>, n
|
|||||||
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, name: s.file.name }))),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const meta = { version: 2, columns: columns.value, negativeSpacingEnabled: negativeSpacingEnabled.value, layers: layersPayload };
|
const meta = {
|
||||||
|
version: 2,
|
||||||
|
columns: columns.value,
|
||||||
|
negativeSpacingEnabled: negativeSpacingEnabled.value,
|
||||||
|
backgroundColor: backgroundColor?.value || 'transparent',
|
||||||
|
layers: layersPayload,
|
||||||
|
};
|
||||||
const metaStr = JSON.stringify(meta, null, 2);
|
const metaStr = JSON.stringify(meta, null, 2);
|
||||||
jsonFolder.file('spritesheet.meta.json', metaStr);
|
jsonFolder.file('spritesheet.meta.json', metaStr);
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user