#6 Add ability to place custom images on custom cards, revise editor UI

This commit is contained in:
Andrio Celos
2024-02-20 16:39:11 +11:00
parent 0b8e59a7e9
commit 8f936d5ed2
6 changed files with 110 additions and 12 deletions

View File

@@ -604,17 +604,25 @@
</main>
<dialog id="galleryCardDialog">
<div id="galleryCardEditor" hidden>
<div id="galleryCardEditorProperties">
<label for="galleryCardEditorRarityBox">Rarity: <select id="galleryCardEditorRarityBox"></select></label>
<textarea type="text" id="galleryCardEditorName" placeholder="Card name"></textarea>
<div id="galleryCardEditorGrid"></div>
<div id="galleryCardEditorSpecialCost">
<label for="galleryCardEditorSpecialCostDefaultBox"><input type="checkbox" id="galleryCardEditorSpecialCostDefaultBox" checked/> Default</label>
</div>
<div id="galleryCardEditorImageToolbar" class="galleryCardEditorToolbar">
<input type="file" id="galleryCardEditorImageFile" accept="image/*" autocomplete="off"/>
<button id="galleryCardEditorImageSelectButton">Choose image</button>
<button id="galleryCardEditorImageClearButton">Clear</button>
<footer>(The image will not be seen by other players.)</footer>
</div>
<div id="galleryCardEditorImageToolbar2" class="galleryCardEditorToolbar">
Colours:
<input type="color" id="galleryCardEditorColour2"/>
<input type="color" id="galleryCardEditorColour1"/>
<select id="galleryCardEditorColourPresetBox"></select>
</div>
<textarea type="text" id="galleryCardEditorName" placeholder="Card name"></textarea>
<div id="galleryCardEditorGrid"></div>
<div id="galleryCardEditorSpecialCost">
<label for="galleryCardEditorSpecialCostDefaultBox"><input type="checkbox" id="galleryCardEditorSpecialCostDefaultBox" checked/> Default</label>
<div id="galleryCardEditorImageToolbar3" class="galleryCardEditorToolbar">
<label for="galleryCardEditorRarityBox">Rarity: <select id="galleryCardEditorRarityBox"></select></label>
</div>
</div>
<form method="dialog">

View File

@@ -129,7 +129,7 @@ class Card {
return true;
}
get isCustom() { return this.number <= CUSTOM_CARD_START; }
get isCustom() { return this.number <= UNSAVED_CUSTOM_CARD_INDEX; }
get isUpcoming() { return this.number < 0 && !this.isCustom; }
get isSpecialWeapon() { return this.specialCost == 3 && this.size == 12 };

View File

@@ -426,7 +426,6 @@ function deckExportJsonReplacer(key: string, value: any) {
case 'number':
case 'altNumber':
case 'artFileName':
case 'imageUrl':
case 'specialCost':
case 'size':
case 'textScale':
@@ -439,6 +438,9 @@ function deckExportJsonReplacer(key: string, value: any) {
case 'line1':
case 'line2':
return value ?? undefined; // Omit null values.
case 'imageUrl':
// Custom cards store image data here, so include it if it is a data URI.
return (<string> value).startsWith('data:') ? value : undefined;
default:
return value;
}

View File

@@ -10,6 +10,9 @@ const bitsToCompleteField = document.getElementById('bitsToCompleteField') as HT
let galleryCardDisplay: CardDisplay | null = null;
let gallerySelectedCardDisplay: CardDisplay | null = null;
const galleryCardEditor = document.getElementById('galleryCardEditor') as HTMLButtonElement;
const galleryCardEditorImageFile = document.getElementById('galleryCardEditorImageFile') as HTMLInputElement;
const galleryCardEditorImageSelectButton = document.getElementById('galleryCardEditorImageSelectButton') as HTMLButtonElement;
const galleryCardEditorImageClearButton = document.getElementById('galleryCardEditorImageClearButton') as HTMLButtonElement;
const galleryCardEditorRarityBox = document.getElementById('galleryCardEditorRarityBox') as HTMLSelectElement;
const galleryCardEditorColour1 = document.getElementById('galleryCardEditorColour1') as HTMLInputElement;
const galleryCardEditorColour2 = document.getElementById('galleryCardEditorColour2') as HTMLInputElement;
@@ -321,6 +324,57 @@ function updateCustomCardSpecialCost() {
}
}
galleryCardEditorImageSelectButton.addEventListener('click', () => galleryCardEditorImageFile.click());
galleryCardEditorImageFile.addEventListener('change', async () => {
if (galleryCardEditorImageFile.files?.length != 1) return;
const originalImage = await createImageBitmap(galleryCardEditorImageFile.files[0]);
var blob = <Blob> galleryCardEditorImageFile.files[0];
if (originalImage.width > 635 || originalImage.height > 885) {
// The entire image will be stored in local storage as a data URI, so downscale larger images.
var width = originalImage.width, height = originalImage.height;
const ratio1 = 635 / width, ratio2 = 885 / height;
if (ratio1 < ratio2) {
width = 635;
height *= ratio1;
} else {
height = 885;
width *= ratio2;
}
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext('2d')!;
ctx.drawImage(originalImage, 0, 0, width, height);
blob = await canvas.convertToBlob({ type: 'image/webp' });
}
// Load image data from the original file or rescaled blob and store it in a data URI.
const url = await new Promise<string>((resolve, _) => {
const reader = new FileReader();
reader.onloadend = () => resolve(<string> reader.result);
reader.readAsDataURL(blob);
});
const display = galleryCardDisplay!;
var image = (<SVGImageElement | undefined> display.element.getElementsByClassName('cardArt')[0]);
if (!image) {
const grid = display.svg.getElementsByClassName('cardGrid')[0];
image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
image.setAttribute('class', 'cardArt');
image.setAttribute('width', '100%');
image.setAttribute('height', '100%');
display.svg.insertBefore(image, grid);
}
image.setAttribute('href', url);
});
galleryCardEditorImageClearButton.addEventListener('click', async () => {
const display = galleryCardDisplay!;
const image = <SVGImageElement | undefined> display.svg.getElementsByClassName('cardArt')[0];
if (image) display.svg.removeChild(image);
});
galleryCardEditorRarityBox.addEventListener('change', () => {
const display = galleryCardDisplay!;
display.element.classList.remove('common');
@@ -332,7 +386,7 @@ galleryCardEditorRarityBox.addEventListener('change', () => {
sizeImage.setAttribute('href', `assets/external/Game Assets/CardCost_0${galleryCardEditorRarityBox.value}.png`);
const backgroundImage = <SVGImageElement> display.svg.getElementsByClassName('cardDisplayBackground')[0];
backgroundImage.setAttribute('href', `assets/CardBackground-${galleryCardEditorRarityBox.value}-1.webp`);
backgroundImage.setAttribute('href', `assets/external/CardBackground-custom-${galleryCardEditorRarityBox.value}-1.webp`);
});
galleryCardEditorColour1.addEventListener('change', galleryCardEditorColour_change);
@@ -382,6 +436,10 @@ galleryCardEditorSubmitButton.addEventListener('click', () => {
const lines = Card.wrapName(galleryCardEditorName.value);
const card = new Card(number, galleryCardEditorName.value.replaceAll('\n', ' '), lines[0], lines[1], parseColour(galleryCardEditorColour1.value), parseColour(galleryCardEditorColour2.value),
<Rarity> parseInt(galleryCardEditorRarityBox.value), customCardSpecialCost, Array.from(galleryCardEditorGridButtons, r => Array.from(r, b => parseInt(b.dataset.state!))));
const image = <SVGImageElement | undefined> galleryCardDisplay!.svg.getElementsByClassName('cardArt')[0];
if (image) card.imageUrl = image.href.baseVal;
if (isNew) {
cardDatabase.customCards.push(card);
addCardToGallery(card);

View File

@@ -374,7 +374,7 @@ deckSelectionForm.addEventListener('submit', e => {
data.append('deckCards', selectedDeck.cards.join('+'));
data.append('deckSleeves', selectedDeck.sleeves.toString());
if (anyCustomCards)
data.append('customCards', JSON.stringify(customCards, deckExportJsonReplacer));
data.append('customCards', JSON.stringify(customCards, submitCustomCardsJsonReplacer));
req.send(data.toString());
localStorage.setItem('lastDeckName', selectedDeck.name);
@@ -382,6 +382,10 @@ deckSelectionForm.addEventListener('submit', e => {
lobbyDeckSubmitButton.disabled = true;
});
function submitCustomCardsJsonReplacer(key: string, value: any) {
return key == 'imageUrl' ? undefined : deckExportJsonReplacer(key, value);
}
stageRandomButton.buttonElement.addEventListener('click', () => {
stageRandomButton.checked = true;
stageButtons.deselect();

View File

@@ -1897,8 +1897,8 @@ button.dragging {
position: absolute;
left: 20%;
right: 20%;
top: 30%;
bottom: 30%;
top: 25%;
aspect-ratio: 1;
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
@@ -1931,6 +1931,32 @@ button.dragging {
background: url('assets/SpecialOverlay.png') center/cover, var(--special-colour-1);
}
.galleryCardEditorToolbar {
position: absolute;
right: 5%;
font: 33% 'Splatoon 2', sans-serif;
background: grey;
padding: 0 0.25em;
}
.galleryCardEditorToolbar footer {
font-size: 60%;
}
.galleryCardEditorToolbar:nth-last-child(3) { bottom: 18%; }
.galleryCardEditorToolbar:nth-last-child(2) { bottom: 12%; }
.galleryCardEditorToolbar:nth-last-child(1) { bottom: 6%; }
#galleryCardEditorImageFile { display: none; }
#galleryCardEditorImageToolbar2 input {
width: 3em;
}
#galleryCardEditorColourPresetBox {
width: 1.8em;
}
#galleryCardEditorSpecialCost {
display: none;
position: absolute;