310 lines
		
	
	
		
			No EOL
		
	
	
		
			10 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			310 lines
		
	
	
		
			No EOL
		
	
	
		
			10 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>
 | |
| 
 | |
|     
 | |
| 
 | |
| 
 | |
|       <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</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>
 | |
|             <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");
 | |
| 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("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);
 | |
| });
 | |
| 
 | |
| // 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 %} |