Merge pull request 'Card Creator' (#51) from card-creator into dev

Reviewed-on: #51
This commit is contained in:
waifu 2025-07-16 20:04:08 -07:00
commit 61cf469d80
10 changed files with 235 additions and 1 deletions

View file

@ -93,6 +93,10 @@ def about():
def submit_character(): def submit_character():
return render_template('submit.html') return render_template('submit.html')
@app.route('/card_creator')
def card_creator():
return render_template('card_creator.html')
@app.route('/players') @app.route('/players')
def player_list(): def player_list():
# Replace with your actual player fetching logic # Replace with your actual player fetching logic

BIN
web/static/card-frame.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View file

@ -41,7 +41,6 @@ body {
} }
.container { .container {
max-width: 800px;
margin: 30px auto; margin: 30px auto;
padding: 20px; padding: 20px;
background-color: #ffffff; background-color: #ffffff;

BIN
web/static/v1_common.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
web/static/v1_epic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
web/static/v1_legendary.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
web/static/v1_rare.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
web/static/v1_uncommon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View file

@ -40,6 +40,7 @@ along with this program. If not, see https://www.gnu.org/licenses/.
<a href="{{ url_for('about') }}">About</a> <a href="{{ url_for('about') }}">About</a>
<a href="{{ url_for('submit_character') }}">Submit a Character</a> <a href="{{ url_for('submit_character') }}">Submit a Character</a>
<a href="{{ url_for('player_list') }}">Player List</a> <a href="{{ url_for('player_list') }}">Player List</a>
<a href="{{ url_for('card_creator') }}">Card Creator</a>
</nav> </nav>
</header> </header>

View file

@ -0,0 +1,230 @@
{% extends "_base.html" %}
{% block content %}
<style>
button {
background: #6aa1ff;
color: white;
border: none;
padding: 0.5rem 1rem;
font-weight: bold;
border-radius: 8px;
cursor: pointer;
}
button:hover {
background: #4c87e4;
}
input, textarea {
padding: 0.5rem;
font-size: 1rem;
border-radius: 8px;
border: 1px solid #ccc;
box-sizing: border-box;
}
input[type="file"] {
padding: 0;
}
</style>
<div class="container" style="max-width: 1100px; margin: auto; padding: 2rem;">
<h1 style="margin-bottom: 2rem;">Card Creator</h1>
<div style="display: flex; gap: 2rem; align-items: flex-start;">
<!-- Left: Inputs -->
<div style="flex: 1;">
<input type="text" id="nameInput" placeholder="Card Name" style="width: 100%; margin-bottom: 0.5rem;"><br>
<input type="text" id="packInput" placeholder="Card Name" style="width: 100%; margin-bottom: 0.5rem;"><br>
<input type="number" id="powerInput" placeholder="⚡ Power" min="0" max="999" style="width: 100%; margin-bottom: 0.5rem;"><br>
<input type="number" id="charmInput" placeholder="❤️ Charm" min="0" max="999" style="width: 100%; margin-bottom: 0.5rem;"><br>
<input type="number" id="witInput" placeholder="💫 Wit" min="0" max="999" style="width: 100%; margin-bottom: 0.5rem;"><br>
<textarea id="flavorInput" placeholder="Flavor text..." rows="4" style="width: 100%; margin-bottom: 0.5rem;"></textarea><br>
<input type="text" id="artistInput" placeholder="Artist Name" style="width: 100%; margin-bottom: 0.5rem;"><br>
<input type="file" id="uploadArt" style="margin-bottom: 0.5rem;"><br>
<select id="frameSelect" style="width: 100%; margin-bottom: 0.5rem;">
<option value="v1_common.png" selected>Common</option>
<option value="v1_uncommon.png">Uncommon</option>
<option value="v1_rare.png">Rare</option>
<option value="v1_epic.png">Epic</option>
<option value="v1_legendary.png">Legendary</option>
</select>
<button id="downloadBtn" onclick="generateCard()">✨ Create Card</button>
</div>
<!-- Right: SVG Card Output -->
<div id="cardOutput" style="flex: 1; border: 1px solid #ccc; border-radius: 16px; background: #fff; box-shadow: 0 4px 10px rgba(0,0,0,0.1);">
<canvas id="cardCanvas" width="800" height="1120"></canvas>
</div>
<!-- Hidden SVG Template -->
</div>
<script>
const canvas = document.getElementById("cardCanvas");
const ctx = canvas.getContext("2d");
const nameInput = document.getElementById("nameInput");
const packInput = document.getElementById("packInput");
const powerInput = document.getElementById("powerInput");
const charmInput = document.getElementById("charmInput");
const witInput = document.getElementById("witInput");
const flavorInput = document.getElementById("flavorInput");
const artistInput = document.getElementById("artistInput");
const downloadBtn = document.getElementById("downloadBtn");
const uploadArt = document.getElementById("uploadArt");
const frameSelect = document.getElementById("frameSelect");
let frameImage = new Image();
let uploadedImage = null;
// Generate a unique ID for the card based on pack and card name
async function generateCardId(...args) {
const encoder = new TextEncoder();
const data = encoder.encode(args.join(':'));
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
// Convert hash to hex
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
// Return first 12 characters
return hex;
}
uploadArt.addEventListener("change", function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
uploadedImage = new Image();
uploadedImage.onload = drawCard;
uploadedImage.src = event.target.result;
};
reader.readAsDataURL(file);
});
// Update frame image when selection changes
function updateFrame() {
frameImage.src = "{{ url_for('static', filename='') }}" + frameSelect.value;
frameImage.onload = drawCard;
}
// Initial frame load
updateFrame();
// Change frame when dropdown changes
frameSelect.addEventListener("change", updateFrame);
function drawCard() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw uploaded art as cropped and centered background if present
if (uploadedImage) {
const targetWidth = 599;
const targetHeight = 745;
const x = (canvas.width - targetWidth) / 2;
const y = ((canvas.height - targetHeight) / 2)-78;
// Calculate cropping for source image to fit aspect ratio
const srcAspect = uploadedImage.width / uploadedImage.height;
const targetAspect = targetWidth / targetHeight;
let srcX = 0, srcY = 0, srcW = uploadedImage.width, srcH = uploadedImage.height;
if (srcAspect > targetAspect) {
// Source is wider: crop sides
srcW = uploadedImage.height * targetAspect;
srcX = (uploadedImage.width - srcW) / 2;
} else {
// Source is taller: crop top/bottom
srcH = uploadedImage.width / targetAspect;
srcY = (uploadedImage.height - srcH) / 2;
}
ctx.drawImage(
uploadedImage,
srcX, srcY, srcW, srcH, // source crop
x, y, targetWidth, targetHeight // destination on canvas
);
}
ctx.drawImage(frameImage, 0, 0, canvas.width, canvas.height);
// Name & pack
ctx.fillStyle = "#FFFFFF";
ctx.font = "bold 40px sans-serif";
ctx.textAlign = "center";
ctx.fillText(nameInput.value, canvas.width / 2, 100);
ctx.font = "20px sans-serif";
ctx.fillText(packInput.value, canvas.width / 2, 160);
ctx.textAlign = "left";
// Stats
ctx.font = "30px sans-serif";
ctx.fillText(powerInput.value, 220, 823);
ctx.fillText(charmInput.value, 373, 823);
ctx.fillText(witInput.value, 512, 823);
// Flavor
ctx.font = "italic 24px serif";
ctx.textAlign = "center";
wrapText(flavorInput.value, canvas.width / 2, 965, 558, 30);
// Artist
ctx.font = "30px sans-serif";
ctx.textAlign = "center";
ctx.fillStyle = "#000000";
ctx.fillText(artistInput.value, canvas.width / 2, 1060);
// Defer ID drawing to a separate async step!
drawCardId();
}
async function drawCardId() {
const id = await generateCardId(packInput.value, nameInput.value, powerInput.value, charmInput.value, witInput.value, artistInput.value);
ctx.font = "15px sans-serif";
ctx.fillText("KC-" + id, canvas.width/2, canvas.height - 30);
}
function wrapText(text, x, y, maxWidth, lineHeight) {
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
let words = lines[i].split(" ");
let line = "";
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + " ";
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
ctx.fillText(line, x, y);
line = words[n] + " ";
y += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line, x, y);
y += lineHeight;
}
}
// Update card live when inputs change
[nameInput, powerInput, charmInput, witInput, flavorInput,packInput,artistInput].forEach(input => {
input.addEventListener("input", drawCard);
});
// Download button
downloadBtn.addEventListener("click", () => {
const link = document.createElement("a");
link.download = "kemoverse-card.webp";
link.href = canvas.toDataURL("image/webp", 0.95);
link.click();
});
</script>
{% endblock %}