diff --git a/TableturfBattleClient/index.html b/TableturfBattleClient/index.html index 5003e8d..56a4da2 100644 --- a/TableturfBattleClient/index.html +++ b/TableturfBattleClient/index.html @@ -604,17 +604,25 @@ - - Rarity: + + + + Default + + + + Choose image + Clear + + + Colours: - - - - Default + + Rarity: diff --git a/TableturfBattleClient/src/Card.ts b/TableturfBattleClient/src/Card.ts index 4311ce4..618beab 100644 --- a/TableturfBattleClient/src/Card.ts +++ b/TableturfBattleClient/src/Card.ts @@ -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 }; diff --git a/TableturfBattleClient/src/Pages/DeckListPage.ts b/TableturfBattleClient/src/Pages/DeckListPage.ts index 4e418d3..15f88a6 100644 --- a/TableturfBattleClient/src/Pages/DeckListPage.ts +++ b/TableturfBattleClient/src/Pages/DeckListPage.ts @@ -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 ( value).startsWith('data:') ? value : undefined; default: return value; } diff --git a/TableturfBattleClient/src/Pages/GalleryPage.ts b/TableturfBattleClient/src/Pages/GalleryPage.ts index 4ec73a5..8a52f9c 100644 --- a/TableturfBattleClient/src/Pages/GalleryPage.ts +++ b/TableturfBattleClient/src/Pages/GalleryPage.ts @@ -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 = 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((resolve, _) => { + const reader = new FileReader(); + reader.onloadend = () => resolve( reader.result); + reader.readAsDataURL(blob); + }); + + const display = galleryCardDisplay!; + var image = ( 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 = 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 = 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), parseInt(galleryCardEditorRarityBox.value), customCardSpecialCost, Array.from(galleryCardEditorGridButtons, r => Array.from(r, b => parseInt(b.dataset.state!)))); + + const image = galleryCardDisplay!.svg.getElementsByClassName('cardArt')[0]; + if (image) card.imageUrl = image.href.baseVal; + if (isNew) { cardDatabase.customCards.push(card); addCardToGallery(card); diff --git a/TableturfBattleClient/src/Pages/LobbyPage.ts b/TableturfBattleClient/src/Pages/LobbyPage.ts index c4c3695..7d41f95 100644 --- a/TableturfBattleClient/src/Pages/LobbyPage.ts +++ b/TableturfBattleClient/src/Pages/LobbyPage.ts @@ -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(); diff --git a/TableturfBattleClient/tableturf.css b/TableturfBattleClient/tableturf.css index e8d2ea5..b1f691f 100644 --- a/TableturfBattleClient/tableturf.css +++ b/TableturfBattleClient/tableturf.css @@ -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;