|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Minecraft Pixel Art Converter</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.11.0/dist/tf.min.js"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> |
|
|
<script> |
|
|
tailwind.config = { |
|
|
theme: { |
|
|
extend: { |
|
|
colors: { |
|
|
minecraft: { |
|
|
green: '#5b9c64', |
|
|
dark: '#3a3a3a', |
|
|
light: '#f0f0f0', |
|
|
accent: '#7aa3d9' |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
</script> |
|
|
<style> |
|
|
.block-item { |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
.block-item:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1); |
|
|
} |
|
|
.pixel-grid { |
|
|
display: grid; |
|
|
gap: 0; |
|
|
margin: 0 auto; |
|
|
} |
|
|
.pixel { |
|
|
border: 1px solid rgba(0,0,0,0.1); |
|
|
} |
|
|
.drop-area { |
|
|
border: 2px dashed #cbd5e0; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
.drop-area.active { |
|
|
border-color: #7aa3d9; |
|
|
background-color: rgba(122, 163, 217, 0.1); |
|
|
} |
|
|
.canvas-container { |
|
|
position: relative; |
|
|
overflow: auto; |
|
|
max-width: 100%; |
|
|
} |
|
|
canvas { |
|
|
max-width: 100%; |
|
|
height: auto; |
|
|
display: block; |
|
|
} |
|
|
.category-title { |
|
|
position: sticky; |
|
|
top: 0; |
|
|
z-index: 10; |
|
|
} |
|
|
.export-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(20px, 1fr)); |
|
|
gap: 1px; |
|
|
} |
|
|
.export-cell { |
|
|
width: 100%; |
|
|
padding-bottom: 100%; |
|
|
position: relative; |
|
|
} |
|
|
.export-cell-inner { |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
font-size: 8px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-minecraft-light min-h-screen font-sans"> |
|
|
|
|
|
<header class="bg-minecraft-dark text-white py-4 shadow-md"> |
|
|
<div class="container mx-auto px-4 flex items-center"> |
|
|
<i class="fas fa-cube text-minecraft-green text-2xl mr-3"></i> |
|
|
<h1 class="text-2xl font-bold">Minecraft Pixel Art Converter</h1> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
<main class="container mx-auto px-4 py-8"> |
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
|
|
|
|
|
<div class="lg:col-span-1"> |
|
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6"> |
|
|
<h2 class="text-xl font-bold text-gray-800 mb-4">Upload Image</h2> |
|
|
<div class="drop-area rounded-lg p-8 text-center cursor-pointer mb-4" id="drop-area"> |
|
|
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-3"></i> |
|
|
<p class="text-gray-600 mb-2">Drag & drop your image here</p> |
|
|
<p class="text-gray-500 text-sm mb-4">or</p> |
|
|
<label for="file-upload" class="bg-minecraft-green hover:bg-green-600 text-white font-medium py-2 px-4 rounded cursor-pointer transition"> |
|
|
<i class="fas fa-folder-open mr-2"></i>Choose File |
|
|
</label> |
|
|
<input type="file" id="file-upload" class="hidden" accept="image/jpeg,image/png"> |
|
|
</div> |
|
|
<p class="text-gray-500 text-sm text-center">Supported formats: JPG, PNG</p> |
|
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6"> |
|
|
<h2 class="text-xl font-bold text-gray-800 mb-4">Original Image</h2> |
|
|
<div class="border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center min-h-[200px]"> |
|
|
<img id="original-preview" class="max-w-full max-h-80 object-contain hidden"> |
|
|
<div id="original-placeholder" class="text-gray-500 text-center p-4"> |
|
|
<i class="fas fa-image text-4xl mb-3"></i> |
|
|
<p>Your image will appear here</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
|
<h2 class="text-xl font-bold text-gray-800 mb-4">Conversion Settings</h2> |
|
|
<div class="mb-4"> |
|
|
<label class="block text-gray-700 mb-2">Pixel Art Size</label> |
|
|
<div class="flex items-center"> |
|
|
<input type="range" id="resolution-slider" min="16" max="128" value="64" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
|
|
<span id="resolution-value" class="ml-4 text-gray-700 font-medium">64x64</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="flex space-x-4"> |
|
|
<button id="convert-btn" class="flex-1 bg-minecraft-green hover:bg-green-600 text-white font-medium py-2 px-4 rounded transition disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
|
|
<i class="fas fa-magic mr-2"></i>Convert to Pixel Art |
|
|
</button> |
|
|
<button id="reset-btn" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-medium py-2 px-4 rounded transition"> |
|
|
<i class="fas fa-redo mr-2"></i>Reset |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="lg:col-span-1"> |
|
|
<div class="bg-white rounded-lg shadow-md p-6 mb-6"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h2 class="text-xl font-bold text-gray-800">Minecraft Pixel Art</h2> |
|
|
<div class="flex items-center"> |
|
|
<span class="text-gray-600 mr-2">Zoom:</span> |
|
|
<div class="flex space-x-1"> |
|
|
<button id="zoom-out" class="w-8 h-8 bg-gray-200 hover:bg-gray-300 rounded flex items-center justify-center"> |
|
|
<i class="fas fa-search-minus"></i> |
|
|
</button> |
|
|
<button id="zoom-in" class="w-8 h-8 bg-gray-200 hover:bg-gray-300 rounded flex items-center justify-center"> |
|
|
<i class="fas fa-search-plus"></i> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="canvas-container border-2 border-gray-300 rounded-lg min-h-[300px] flex items-center justify-center" id="canvas-container"> |
|
|
<canvas id="pixel-art-canvas" class="hidden"></canvas> |
|
|
<div id="pixel-art-placeholder" class="text-gray-500 text-center p-4"> |
|
|
<i class="fas fa-cubes text-4xl mb-3"></i> |
|
|
<p>Your Minecraft pixel art will appear here</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
|
<h2 class="text-xl font-bold text-gray-800 mb-4">Export Minecraft Art</h2> |
|
|
<button id="export-btn" class="w-full bg-minecraft-accent hover:bg-blue-600 text-white font-medium py-3 px-4 rounded transition mb-4 disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
|
|
<i class="fas fa-download mr-2"></i>Export Minecraft Blueprint |
|
|
</button> |
|
|
|
|
|
<div id="export-results" class="hidden"> |
|
|
<div class="mb-4"> |
|
|
<h3 class="font-bold text-gray-700 mb-2">Art Dimensions</h3> |
|
|
<p id="dimensions" class="text-gray-800">64x64 blocks</p> |
|
|
</div> |
|
|
|
|
|
<div class="mb-4"> |
|
|
<h3 class="font-bold text-gray-700 mb-2">Blocks Required</h3> |
|
|
<div id="block-counts" class="bg-gray-50 rounded-lg p-4 max-h-60 overflow-y-auto"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<h3 class="font-bold text-gray-700 mb-2">Placement Guide</h3> |
|
|
<div class="bg-gray-50 rounded-lg p-4"> |
|
|
<div class="export-grid mb-4" id="placement-grid"> |
|
|
|
|
|
</div> |
|
|
<div class="flex justify-between"> |
|
|
<button id="download-txt" class="text-minecraft-accent hover:text-blue-600 font-medium"> |
|
|
<i class="fas fa-file-alt mr-2"></i>Download Text Guide |
|
|
</button> |
|
|
<button id="download-grid" class="text-minecraft-accent hover:text-blue-600 font-medium"> |
|
|
<i class="fas fa-table mr-2"></i>Download Grid Guide |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="lg:col-span-1"> |
|
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h2 class="text-xl font-bold text-gray-800">Minecraft Block Palette</h2> |
|
|
<div class="flex space-x-2"> |
|
|
<button id="select-all" class="text-sm bg-gray-200 hover:bg-gray-300 text-gray-800 py-1 px-3 rounded transition"> |
|
|
<i class="fas fa-check-square mr-1"></i>Select All |
|
|
</button> |
|
|
<button id="deselect-all" class="text-sm bg-gray-200 hover:bg-gray-300 text-gray-800 py-1 px-3 rounded transition"> |
|
|
<i class="fas fa-square mr-1"></i>Deselect All |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="mb-4"> |
|
|
<input type="text" id="block-search" placeholder="Search blocks..." class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-minecraft-accent"> |
|
|
</div> |
|
|
|
|
|
<div class="block-palette-container max-h-[calc(100vh-250px)] overflow-y-auto pr-2"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</main> |
|
|
|
|
|
<script> |
|
|
|
|
|
const blockPalette = [ |
|
|
|
|
|
{ name: "Clay", category: "Easily Findable", color: [158, 164, 176] }, |
|
|
{ name: "Jungle log", category: "Easily Findable", color: [87, 68, 27] }, |
|
|
{ name: "Birch log", category: "Easily Findable", color: [207, 206, 201] }, |
|
|
{ name: "Oak log", category: "Easily Findable", color: [102, 81, 50] }, |
|
|
{ name: "Spruce log", category: "Easily Findable", color: [46, 29, 12] }, |
|
|
{ name: "Stone slab", category: "Easily Findable", color: [125, 125, 125] }, |
|
|
{ name: "Gravel", category: "Easily Findable", color: [132, 127, 127] }, |
|
|
{ name: "Sandstone", category: "Easily Findable", color: [219, 211, 160] }, |
|
|
{ name: "White wool", category: "Easily Findable", color: [233, 236, 236] }, |
|
|
{ name: "Blue wool", category: "Easily Findable", color: [53, 57, 157] }, |
|
|
{ name: "Yellow wool", category: "Easily Findable", color: [248, 198, 40] }, |
|
|
{ name: "Red wool", category: "Easily Findable", color: [161, 39, 35] }, |
|
|
{ name: "Green wool", category: "Easily Findable", color: [86, 110, 44] }, |
|
|
{ name: "Snow block", category: "Easily Findable", color: [240, 251, 251] }, |
|
|
{ name: "Cobblestone", category: "Easily Findable", color: [125, 125, 125] }, |
|
|
{ name: "Sand", category: "Easily Findable", color: [219, 211, 160] }, |
|
|
{ name: "Dirt", category: "Easily Findable", color: [134, 96, 67] }, |
|
|
|
|
|
|
|
|
{ name: "Bookshelf", category: "Manufactured", color: [178, 141, 86] }, |
|
|
{ name: "Block of Iron", category: "Manufactured", color: [220, 220, 220] }, |
|
|
{ name: "Bricks", category: "Manufactured", color: [150, 99, 83] }, |
|
|
{ name: "Stone brick", category: "Manufactured", color: [122, 122, 122] }, |
|
|
{ name: "Blue terracotta", category: "Manufactured", color: [74, 60, 91] }, |
|
|
{ name: "White terracotta", category: "Manufactured", color: [210, 178, 162] }, |
|
|
{ name: "Black concrete", category: "Manufactured", color: [9, 11, 16] }, |
|
|
{ name: "Block of Copper", category: "Manufactured", color: [198, 112, 100] }, |
|
|
|
|
|
|
|
|
{ name: "Glass", category: "Transparent", color: [155, 186, 178, 200] }, |
|
|
{ name: "Black stained glass", category: "Transparent", color: [25, 25, 25, 200] }, |
|
|
{ name: "Blue stained glass", category: "Transparent", color: [51, 76, 178, 200] }, |
|
|
{ name: "Green stained glass", category: "Transparent", color: [102, 127, 51, 200] }, |
|
|
{ name: "Oak leaves", category: "Transparent", color: [48, 112, 20, 200] }, |
|
|
|
|
|
|
|
|
{ name: "Block of Diamond", category: "Rare", color: [99, 219, 213] }, |
|
|
{ name: "Block of Emerald", category: "Rare", color: [42, 204, 112] }, |
|
|
{ name: "Block of Gold", category: "Rare", color: [249, 236, 50] }, |
|
|
{ name: "Obsidian", category: "Rare", color: [20, 18, 30] }, |
|
|
{ name: "Prismarine bricks", category: "Rare", color: [100, 160, 144] }, |
|
|
{ name: "Slime Block", category: "Rare", color: [128, 189, 90] } |
|
|
]; |
|
|
|
|
|
|
|
|
let state = { |
|
|
originalImage: null, |
|
|
pixelArtData: null, |
|
|
selectedBlocks: new Set(blockPalette.map(block => block.name)), |
|
|
resolution: 64, |
|
|
zoomLevel: 5, |
|
|
conversionInProgress: false |
|
|
}; |
|
|
|
|
|
|
|
|
const dom = { |
|
|
dropArea: document.getElementById('drop-area'), |
|
|
fileUpload: document.getElementById('file-upload'), |
|
|
originalPreview: document.getElementById('original-preview'), |
|
|
originalPlaceholder: document.getElementById('original-placeholder'), |
|
|
convertBtn: document.getElementById('convert-btn'), |
|
|
resetBtn: document.getElementById('reset-btn'), |
|
|
pixelArtCanvas: document.getElementById('pixel-art-canvas'), |
|
|
pixelArtPlaceholder: document.getElementById('pixel-art-placeholder'), |
|
|
canvasContainer: document.getElementById('canvas-container'), |
|
|
blockPaletteContainer: document.querySelector('.block-palette-container'), |
|
|
blockSearch: document.getElementById('block-search'), |
|
|
selectAllBtn: document.getElementById('select-all'), |
|
|
deselectAllBtn: document.getElementById('deselect-all'), |
|
|
resolutionSlider: document.getElementById('resolution-slider'), |
|
|
resolutionValue: document.getElementById('resolution-value'), |
|
|
zoomInBtn: document.getElementById('zoom-in'), |
|
|
zoomOutBtn: document.getElementById('zoom-out'), |
|
|
exportBtn: document.getElementById('export-btn'), |
|
|
exportResults: document.getElementById('export-results'), |
|
|
dimensions: document.getElementById('dimensions'), |
|
|
blockCounts: document.getElementById('block-counts'), |
|
|
placementGrid: document.getElementById('placement-grid'), |
|
|
downloadTxt: document.getElementById('download-txt'), |
|
|
downloadGrid: document.getElementById('download-grid') |
|
|
}; |
|
|
|
|
|
|
|
|
function init() { |
|
|
renderBlockPalette(); |
|
|
setupEventListeners(); |
|
|
} |
|
|
|
|
|
|
|
|
function renderBlockPalette() { |
|
|
|
|
|
const categories = {}; |
|
|
blockPalette.forEach(block => { |
|
|
if (!categories[block.category]) { |
|
|
categories[block.category] = []; |
|
|
} |
|
|
categories[block.category].push(block); |
|
|
}); |
|
|
|
|
|
|
|
|
let html = ''; |
|
|
for (const category in categories) { |
|
|
html += ` |
|
|
<div class="mb-6"> |
|
|
<h3 class="category-title bg-gray-100 text-gray-800 font-bold py-2 px-3 rounded-t-lg">${category}</h3> |
|
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 p-3 bg-gray-50 rounded-b-lg"> |
|
|
${categories[category].map(block => ` |
|
|
<div class="block-item bg-white rounded-lg overflow-hidden shadow-sm border border-gray-200 ${state.selectedBlocks.has(block.name) ? 'border-minecraft-green' : ''}"> |
|
|
<label class="flex items-center p-2 cursor-pointer"> |
|
|
<input type="checkbox" class="block-checkbox mr-2" data-block="${block.name}" ${state.selectedBlocks.has(block.name) ? 'checked' : ''}> |
|
|
<div class="flex items-center"> |
|
|
<div class="w-4 h-4 rounded-sm mr-2" style="background-color: rgb(${block.color.join(',')})"></div> |
|
|
<span class="text-sm text-gray-700 truncate">${block.name}</span> |
|
|
</div> |
|
|
</label> |
|
|
</div> |
|
|
`).join('')} |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
dom.blockPaletteContainer.innerHTML = html; |
|
|
} |
|
|
|
|
|
|
|
|
function setupEventListeners() { |
|
|
|
|
|
dom.dropArea.addEventListener('dragover', (e) => { |
|
|
e.preventDefault(); |
|
|
dom.dropArea.classList.add('active'); |
|
|
}); |
|
|
|
|
|
dom.dropArea.addEventListener('dragleave', () => { |
|
|
dom.dropArea.classList.remove('active'); |
|
|
}); |
|
|
|
|
|
dom.dropArea.addEventListener('drop', (e) => { |
|
|
e.preventDefault(); |
|
|
dom.dropArea.classList.remove('active'); |
|
|
|
|
|
if (e.dataTransfer.files.length) { |
|
|
handleFileUpload(e.dataTransfer.files[0]); |
|
|
} |
|
|
}); |
|
|
|
|
|
dom.fileUpload.addEventListener('change', (e) => { |
|
|
if (e.target.files.length) { |
|
|
handleFileUpload(e.target.files[0]); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
dom.blockPaletteContainer.addEventListener('change', (e) => { |
|
|
if (e.target.classList.contains('block-checkbox')) { |
|
|
const blockName = e.target.dataset.block; |
|
|
if (e.target.checked) { |
|
|
state.selectedBlocks.add(blockName); |
|
|
} else { |
|
|
state.selectedBlocks.delete(blockName); |
|
|
} |
|
|
|
|
|
const blockItem = e.target.closest('.block-item'); |
|
|
if (e.target.checked) { |
|
|
blockItem.classList.add('border-minecraft-green'); |
|
|
} else { |
|
|
blockItem.classList.remove('border-minecraft-green'); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
dom.blockSearch.addEventListener('input', (e) => { |
|
|
const searchTerm = e.target.value.toLowerCase(); |
|
|
document.querySelectorAll('.block-item').forEach(item => { |
|
|
const blockName = item.querySelector('span').textContent.toLowerCase(); |
|
|
if (blockName.includes(searchTerm)) { |
|
|
item.style.display = 'block'; |
|
|
} else { |
|
|
item.style.display = 'none'; |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
dom.selectAllBtn.addEventListener('click', () => { |
|
|
state.selectedBlocks = new Set(blockPalette.map(block => block.name)); |
|
|
document.querySelectorAll('.block-checkbox').forEach(checkbox => { |
|
|
checkbox.checked = true; |
|
|
}); |
|
|
document.querySelectorAll('.block-item').forEach(item => { |
|
|
item.classList.add('border-minecraft-green'); |
|
|
}); |
|
|
}); |
|
|
|
|
|
dom.deselectAllBtn.addEventListener('click', () => { |
|
|
state.selectedBlocks.clear(); |
|
|
document.querySelectorAll('.block-checkbox').forEach(checkbox => { |
|
|
checkbox.checked = false; |
|
|
}); |
|
|
document.querySelectorAll('.block-item').forEach(item => { |
|
|
item.classList.remove('border-minecraft-green'); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
dom.resolutionSlider.addEventListener('input', (e) => { |
|
|
state.resolution = parseInt(e.target.value); |
|
|
dom.resolutionValue.textContent = `${state.resolution}x${state.resolution}`; |
|
|
}); |
|
|
|
|
|
|
|
|
dom.zoomInBtn.addEventListener('click', () => { |
|
|
state.zoomLevel = Math.min(state.zoomLevel + 1, 10); |
|
|
renderPixelArt(); |
|
|
}); |
|
|
|
|
|
dom.zoomOutBtn.addEventListener('click', () => { |
|
|
state.zoomLevel = Math.max(state.zoomLevel - 1, 1); |
|
|
renderPixelArt(); |
|
|
}); |
|
|
|
|
|
|
|
|
dom.convertBtn.addEventListener('click', convertImageToPixelArt); |
|
|
|
|
|
|
|
|
dom.resetBtn.addEventListener('click', resetApp); |
|
|
|
|
|
|
|
|
dom.exportBtn.addEventListener('click', exportArt); |
|
|
|
|
|
|
|
|
dom.downloadTxt.addEventListener('click', downloadTextGuide); |
|
|
dom.downloadGrid.addEventListener('click', downloadGridGuide); |
|
|
} |
|
|
|
|
|
|
|
|
function handleFileUpload(file) { |
|
|
if (!file.type.match('image.*')) { |
|
|
alert('Please upload an image file (JPG or PNG)'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const reader = new FileReader(); |
|
|
reader.onload = (e) => { |
|
|
state.originalImage = new Image(); |
|
|
state.originalImage.onload = () => { |
|
|
|
|
|
dom.originalPreview.src = e.target.result; |
|
|
dom.originalPreview.classList.remove('hidden'); |
|
|
dom.originalPlaceholder.classList.add('hidden'); |
|
|
|
|
|
|
|
|
dom.convertBtn.disabled = false; |
|
|
}; |
|
|
state.originalImage.src = e.target.result; |
|
|
}; |
|
|
reader.readAsDataURL(file); |
|
|
} |
|
|
|
|
|
|
|
|
async function convertImageToPixelArt() { |
|
|
if (!state.originalImage || state.conversionInProgress) return; |
|
|
|
|
|
state.conversionInProgress = true; |
|
|
dom.convertBtn.disabled = true; |
|
|
dom.convertBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Processing...'; |
|
|
|
|
|
try { |
|
|
|
|
|
const selectedBlocks = blockPalette.filter(block => |
|
|
state.selectedBlocks.has(block.name) |
|
|
); |
|
|
|
|
|
if (selectedBlocks.length === 0) { |
|
|
alert('Please select at least one block from the palette'); |
|
|
state.conversionInProgress = false; |
|
|
dom.convertBtn.disabled = false; |
|
|
dom.convertBtn.innerHTML = '<i class="fas fa-magic mr-2"></i>Convert to Pixel Art'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const tensor = tf.browser.fromPixels(state.originalImage); |
|
|
const resized = tf.image.resizeBilinear(tensor, [state.resolution, state.resolution]); |
|
|
|
|
|
|
|
|
const blockColors = selectedBlocks.map(block => { |
|
|
const color = block.color.length === 4 ? block.color.slice(0, 3) : block.color; |
|
|
return tf.tensor1d(color); |
|
|
}); |
|
|
|
|
|
const pixelArtData = []; |
|
|
const pixels = resized.arraySync(); |
|
|
|
|
|
for (let y = 0; y < state.resolution; y++) { |
|
|
const row = []; |
|
|
for (let x = 0; x < state.resolution; x++) { |
|
|
const pixel = pixels[y][x]; |
|
|
let minDistance = Infinity; |
|
|
let closestBlockIndex = 0; |
|
|
|
|
|
|
|
|
for (let i = 0; i < blockColors.length; i++) { |
|
|
const color = blockColors[i]; |
|
|
const distance = Math.sqrt( |
|
|
Math.pow(pixel[0] - color.arraySync()[0], 2) + |
|
|
Math.pow(pixel[1] - color.arraySync()[1], 2) + |
|
|
Math.pow(pixel[2] - color.arraySync()[2], 2) |
|
|
); |
|
|
|
|
|
if (distance < minDistance) { |
|
|
minDistance = distance; |
|
|
closestBlockIndex = i; |
|
|
} |
|
|
} |
|
|
|
|
|
row.push({ |
|
|
block: selectedBlocks[closestBlockIndex], |
|
|
index: closestBlockIndex |
|
|
}); |
|
|
} |
|
|
pixelArtData.push(row); |
|
|
} |
|
|
|
|
|
|
|
|
tf.dispose([tensor, resized]); |
|
|
blockColors.forEach(t => t.dispose()); |
|
|
|
|
|
|
|
|
state.pixelArtData = pixelArtData; |
|
|
|
|
|
|
|
|
renderPixelArt(); |
|
|
|
|
|
|
|
|
dom.exportBtn.disabled = false; |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Conversion error:', error); |
|
|
alert('An error occurred during conversion. Please try again.'); |
|
|
} finally { |
|
|
state.conversionInProgress = false; |
|
|
dom.convertBtn.disabled = false; |
|
|
dom.convertBtn.innerHTML = '<i class="fas fa-magic mr-2"></i>Convert to Pixel Art'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function renderPixelArt() { |
|
|
if (!state.pixelArtData) return; |
|
|
|
|
|
const canvas = dom.pixelArtCanvas; |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const resolution = state.resolution; |
|
|
const pixelSize = state.zoomLevel; |
|
|
|
|
|
canvas.width = resolution * pixelSize; |
|
|
canvas.height = resolution * pixelSize; |
|
|
canvas.classList.remove('hidden'); |
|
|
dom.pixelArtPlaceholder.classList.add('hidden'); |
|
|
|
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
|
|
|
|
|
|
|
for (let y = 0; y < resolution; y++) { |
|
|
for (let x = 0; x < resolution; x++) { |
|
|
const pixel = state.pixelArtData[y][x]; |
|
|
const color = pixel.block.color; |
|
|
|
|
|
ctx.fillStyle = `rgb(${color.slice(0, 3).join(',')})`; |
|
|
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); |
|
|
|
|
|
|
|
|
if (pixelSize > 3) { |
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.1)'; |
|
|
ctx.strokeRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function exportArt() { |
|
|
if (!state.pixelArtData) return; |
|
|
|
|
|
|
|
|
dom.dimensions.textContent = `${state.resolution}x${state.resolution} blocks`; |
|
|
|
|
|
|
|
|
const blockCounts = {}; |
|
|
state.pixelArtData.flat().forEach(pixel => { |
|
|
const blockName = pixel.block.name; |
|
|
blockCounts[blockName] = (blockCounts[blockName] || 0) + 1; |
|
|
}); |
|
|
|
|
|
|
|
|
let countsHTML = ''; |
|
|
Object.entries(blockCounts).sort((a, b) => b[1] - a[1]).forEach(([block, count]) => { |
|
|
countsHTML += ` |
|
|
<div class="flex justify-between py-2 border-b border-gray-200"> |
|
|
<span>${block}</span> |
|
|
<span class="font-medium">${count}</span> |
|
|
</div> |
|
|
`; |
|
|
}); |
|
|
dom.blockCounts.innerHTML = countsHTML; |
|
|
|
|
|
|
|
|
dom.placementGrid.innerHTML = ''; |
|
|
const gridSize = Math.min(20, state.resolution); |
|
|
|
|
|
for (let y = 0; y < gridSize; y++) { |
|
|
for (let x = 0; x < gridSize; x++) { |
|
|
const pixel = state.pixelArtData[y][x]; |
|
|
const color = pixel.block.color; |
|
|
|
|
|
const cell = document.createElement('div'); |
|
|
cell.className = 'export-cell'; |
|
|
|
|
|
const inner = document.createElement('div'); |
|
|
inner.className = 'export-cell-inner'; |
|
|
inner.style.backgroundColor = `rgb(${color.slice(0, 3).join(',')})`; |
|
|
inner.title = pixel.block.name; |
|
|
|
|
|
cell.appendChild(inner); |
|
|
dom.placementGrid.appendChild(cell); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
dom.exportResults.classList.remove('hidden'); |
|
|
|
|
|
|
|
|
dom.exportResults.scrollIntoView({ behavior: 'smooth' }); |
|
|
} |
|
|
|
|
|
|
|
|
function downloadTextGuide() { |
|
|
if (!state.pixelArtData) return; |
|
|
|
|
|
let content = `Minecraft Pixel Art Blueprint\n`; |
|
|
content += `Dimensions: ${state.resolution}x${state.resolution} blocks\n\n`; |
|
|
content += "Block Placement Guide:\n\n"; |
|
|
|
|
|
for (let y = 0; y < state.resolution; y++) { |
|
|
for (let x = 0; x < state.resolution; x++) { |
|
|
const pixel = state.pixelArtData[y][x]; |
|
|
content += `(${x},${y}): ${pixel.block.name}\n`; |
|
|
} |
|
|
} |
|
|
|
|
|
content += "\nBlock Quantities:\n"; |
|
|
const blockCounts = {}; |
|
|
state.pixelArtData.flat().forEach(pixel => { |
|
|
const blockName = pixel.block.name; |
|
|
blockCounts[blockName] = (blockCounts[blockName] || 0) + 1; |
|
|
}); |
|
|
|
|
|
Object.entries(blockCounts).sort((a, b) => b[1] - a[1]).forEach(([block, count]) => { |
|
|
content += `${block}: ${count}\n`; |
|
|
}); |
|
|
|
|
|
const blob = new Blob([content], { type: 'text/plain' }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = 'minecraft-pixel-art-blueprint.txt'; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
URL.revokeObjectURL(url); |
|
|
} |
|
|
|
|
|
|
|
|
function downloadGridGuide() { |
|
|
if (!state.pixelArtData) return; |
|
|
|
|
|
|
|
|
const canvas = document.createElement('canvas'); |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const resolution = state.resolution; |
|
|
const pixelSize = 20; |
|
|
|
|
|
canvas.width = resolution * pixelSize; |
|
|
canvas.height = resolution * pixelSize; |
|
|
|
|
|
|
|
|
for (let y = 0; y < resolution; y++) { |
|
|
for (let x = 0; x < resolution; x++) { |
|
|
const pixel = state.pixelArtData[y][x]; |
|
|
const color = pixel.block.color; |
|
|
|
|
|
ctx.fillStyle = `rgb(${color.slice(0, 3).join(',')})`; |
|
|
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); |
|
|
|
|
|
|
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.1)'; |
|
|
ctx.strokeRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
ctx.fillStyle = '#000'; |
|
|
ctx.font = '20px Arial'; |
|
|
ctx.textAlign = 'center'; |
|
|
ctx.fillText(`Minecraft Pixel Art Blueprint - ${resolution}x${resolution}`, canvas.width / 2, 30); |
|
|
|
|
|
|
|
|
const url = canvas.toDataURL('image/png'); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = 'minecraft-pixel-art-grid.png'; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
} |
|
|
|
|
|
|
|
|
function resetApp() { |
|
|
state.originalImage = null; |
|
|
state.pixelArtData = null; |
|
|
state.selectedBlocks = new Set(blockPalette.map(block => block.name)); |
|
|
|
|
|
|
|
|
dom.originalPreview.classList.add('hidden'); |
|
|
dom.originalPlaceholder.classList.remove('hidden'); |
|
|
dom.pixelArtCanvas.classList.add('hidden'); |
|
|
dom.pixelArtPlaceholder.classList.remove('hidden'); |
|
|
dom.convertBtn.disabled = true; |
|
|
dom.exportBtn.disabled = true; |
|
|
dom.exportResults.classList.add('hidden'); |
|
|
|
|
|
|
|
|
dom.fileUpload.value = ''; |
|
|
|
|
|
|
|
|
renderBlockPalette(); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', init); |
|
|
</script> |
|
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Saad4web/pixelarts" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |