370 lines
No EOL
12 KiB
HTML
370 lines
No EOL
12 KiB
HTML
{% 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;">
|
|
|
|
<p>Use this tool to create custom Kemoverse cards! Fill in the details below, upload your art, and generate a card image.</p>
|
|
<p>Note: The card ID is generated based on the details, so make sure to fill them out!</p>
|
|
|
|
<div style="margin-bottom: 0.5rem;">
|
|
<input type="checkbox" id="restrictStatsToggle" checked>
|
|
<label for="restrictStatsToggle">Restrict stats by rarity</label>
|
|
</div>
|
|
|
|
<input type="text" id="nameInput" placeholder="Card Name" style="width: 100%; margin-bottom: 0.5rem;"><br>
|
|
<input type="text" id="packInput" placeholder="Pack 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 (100)</option>
|
|
<option value="v1_uncommon.png">Uncommon (140)</option>
|
|
<option value="v1_rare.png">Rare (180)</option>
|
|
<option value="v1_epic.png">Epic (220)</option>
|
|
<option value="v1_legendary.png">Legendary (250)</option>
|
|
</select>
|
|
<button id="downloadBtn" onclick="generateCard()">✨ Create Card</button>
|
|
<p>All the processing is done in your browser, no data is sent to the server.</p>
|
|
</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 src="{{ url_for('static', filename='js/jszip.min.js') }}"></script>
|
|
<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");
|
|
const restrictStatsToggle = document.getElementById("restrictStatsToggle");
|
|
let frameImage = new Image();
|
|
let uploadedImage = null;
|
|
|
|
// Rarity caps for stats
|
|
const rarityCaps = {
|
|
"v1_common.png": 100,
|
|
"v1_uncommon.png": 140,
|
|
"v1_rare.png": 180,
|
|
"v1_epic.png": 220,
|
|
"v1_legendary.png": 250
|
|
};
|
|
|
|
// 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);
|
|
// Place a white background
|
|
ctx.fillStyle = "#FFFFFF"; // white background
|
|
// make it smaller than the size of the canvas
|
|
ctx.fillRect(30, 30, canvas.width - 60, canvas.height - 60);
|
|
|
|
// 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("Kemoverse - " + 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);
|
|
});
|
|
|
|
// Enforce stat caps based on rarity and frame
|
|
function enforceStatCap(changedInput) {
|
|
if (!restrictStatsToggle.checked) return;
|
|
const cap = rarityCaps[frameSelect.value] || 9999;
|
|
let power = parseInt(powerInput.value) || 0;
|
|
let charm = parseInt(charmInput.value) || 0;
|
|
let wit = parseInt(witInput.value) || 0;
|
|
let total = power + charm + wit;
|
|
if (total > cap) {
|
|
// Reduce the changed input to fit the cap
|
|
const excess = total - cap;
|
|
if (changedInput === powerInput) {
|
|
power = Math.max(0, power - excess);
|
|
powerInput.value = power;
|
|
} else if (changedInput === charmInput) {
|
|
charm = Math.max(0, charm - excess);
|
|
charmInput.value = charm;
|
|
} else if (changedInput === witInput) {
|
|
wit = Math.max(0, wit - excess);
|
|
witInput.value = wit;
|
|
}
|
|
}
|
|
}
|
|
|
|
[powerInput, charmInput, witInput].forEach(input => {
|
|
input.addEventListener("input", function() {
|
|
enforceStatCap(this);
|
|
drawCard();
|
|
});
|
|
});
|
|
|
|
frameSelect.addEventListener("change", function() {
|
|
if (restrictStatsToggle.checked) {
|
|
enforceStatCap();
|
|
}
|
|
updateFrame();
|
|
});
|
|
|
|
restrictStatsToggle.addEventListener("change", function() {
|
|
if (this.checked) {
|
|
enforceStatCap();
|
|
}
|
|
});
|
|
|
|
// Download button
|
|
downloadBtn.addEventListener("click", () => {
|
|
// Sanitize file name
|
|
const safeName = (nameInput.value || "card").replace(/[^a-z0-9_\-]/gi, "_");
|
|
const safePack = (packInput.value || "pack").replace(/[^a-z0-9_\-]/gi, "_");
|
|
const fileName = `kemoverse_${safePack}_${safeName}.webp`;
|
|
|
|
const link = document.createElement("a");
|
|
link.download = fileName;
|
|
link.href = canvas.toDataURL("image/webp", 0.95);
|
|
link.click();
|
|
});
|
|
|
|
const jsonBtn = document.createElement("button");
|
|
jsonBtn.textContent = "Download Card Info";
|
|
jsonBtn.style.marginTop = "0.5rem";
|
|
jsonBtn.onclick = () => {
|
|
const cardData = {
|
|
name: nameInput.value,
|
|
pack: packInput.value,
|
|
power: powerInput.value,
|
|
charm: charmInput.value,
|
|
wit: witInput.value,
|
|
flavor: flavorInput.value,
|
|
artist: artistInput.value,
|
|
frame: frameSelect.value,
|
|
id: generateCardId(packInput.value, nameInput.value, powerInput.value, charmInput.value, witInput.value, artistInput.value)
|
|
};
|
|
const safeName = (nameInput.value || "card").replace(/[^a-z0-9_\-]/gi, "_");
|
|
const safePack = (packInput.value || "pack").replace(/[^a-z0-9_\-]/gi, "_");
|
|
const fileName = `kemoverse_${safePack}_${safeName}.json`;
|
|
|
|
const blob = new Blob([JSON.stringify(cardData, null, 2)], {type: "application/json"});
|
|
const link = document.createElement("a");
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = fileName;
|
|
link.click();
|
|
};
|
|
|
|
// Place the button below the downloadBtn
|
|
downloadBtn.parentNode.insertBefore(jsonBtn, downloadBtn.nextSibling);
|
|
|
|
const zipBtn = document.createElement("button");
|
|
zipBtn.textContent = "Download Card ZIP";
|
|
zipBtn.style.marginTop = "0.5rem";
|
|
zipBtn.onclick = async () => {
|
|
const cardData = {
|
|
name: nameInput.value,
|
|
pack: packInput.value,
|
|
power: powerInput.value,
|
|
charm: charmInput.value,
|
|
wit: witInput.value,
|
|
flavor: flavorInput.value,
|
|
artist: artistInput.value,
|
|
frame: frameSelect.value,
|
|
id: await generateCardId(packInput.value, nameInput.value, powerInput.value, charmInput.value, witInput.value, artistInput.value)
|
|
};
|
|
const safeName = (nameInput.value || "card").replace(/[^a-z0-9_\-]/gi, "_");
|
|
const safePack = (packInput.value || "pack").replace(/[^a-z0-9_\-]/gi, "_");
|
|
const imgFileName = `kemoverse_${safePack}_${safeName}.webp`;
|
|
const jsonFileName = `kemoverse_${safePack}_${safeName}.json`;
|
|
|
|
// Create ZIP
|
|
const zip = new JSZip();
|
|
// Add image
|
|
const imgData = canvas.toDataURL("image/webp", 0.95).split(',')[1];
|
|
zip.file(imgFileName, imgData, {base64: true});
|
|
// Add JSON
|
|
zip.file(jsonFileName, JSON.stringify(cardData, null, 2));
|
|
|
|
// Generate and download
|
|
const content = await zip.generateAsync({type: "blob"});
|
|
const link = document.createElement("a");
|
|
link.href = URL.createObjectURL(content);
|
|
link.download = `kemoverse_${safePack}_${safeName}.zip`;
|
|
link.click();
|
|
};
|
|
|
|
// Place the button below the downloadBtn
|
|
downloadBtn.parentNode.insertBefore(zipBtn, downloadBtn.nextSibling);
|
|
</script>
|
|
{% endblock %} |