Spaces:
Running
Running
import { toPng } from 'https://cdn.skypack.dev/html-to-image'; | |
/* HTML generation */ | |
const TYPES = { | |
Grass: '🍃', | |
Fire: '🔥', | |
Water: '💧', | |
Lightning: '⚡', | |
Fighting: '✊', | |
Psychic: '👁️', | |
Colorless: '⭐', | |
Darkness: '🌑', | |
Metal: '⚙️', | |
Dragon: '🐲', | |
Fairy: '🧚', | |
}; | |
const energyHTML = (type, types = TYPES) => { | |
return `<span title="${type} energy" class="energy ${type.toLowerCase()}">${types[type]}</span>`; | |
}; | |
const attackCostHTML = (cost) => { | |
if (!cost.length) { | |
return ''; | |
} | |
return ` | |
<div class="attack-cost"> | |
${cost.map((energy) => energyHTML(energy)).join('')} | |
</div>`; | |
}; | |
const attackDescriptionHTML = (text) => { | |
if (!text) { | |
return ''; | |
} | |
let fontSize; | |
if (text.length > 185) { | |
fontSize = 0.7; | |
} else if (text.length > 120) { | |
fontSize = 0.8; | |
} else { | |
fontSize = 0.9; | |
} | |
return `<span class="attack-details"${fontSize ? ` style="font-size: ${fontSize.toString()}em"` : ''}>${text}</span>`; | |
}; | |
const attackDamageHTML = (damage) => { | |
if (!damage) { | |
return ''; | |
} | |
return `<span class="attack-damage">${damage}</span>`; | |
}; | |
const attackRowsHTML = (attacks) => { | |
return attacks | |
.map((attack) => { | |
const { cost, damage, name, text } = attack; | |
return ` | |
<li class="attacks-row ${cost.length ? '' : 'no-cost'} ${damage ? '' : 'no-damage'}"> | |
${attackCostHTML(cost)} | |
<span class="attack-text"> | |
<span class="attack-name">${name}</span> | |
${attackDescriptionHTML(text)} | |
</span> | |
${attackDamageHTML(damage)} | |
</li>`; | |
}) | |
.join(''); | |
}; | |
const cardHTML = (details) => { | |
const { hp, energy_type, species, length, weight, attacks, weakness, resistance, retreat, description, rarity } = | |
details; | |
const poke_name = details.name; // `name` would be reserved JS word | |
return ` | |
<div class="pokecard ${energy_type.toLowerCase()}" data-displayed="true"> | |
<p class="evolves">Basic Pokémon</p> | |
<header> | |
<h1 class="name">${poke_name}</h1> | |
<div> | |
<span class="hp">${hp} HP</span> | |
${energyHTML(energy_type)} | |
</div> | |
</header> | |
<img class="picture frame" alt="AI generated Pokémon called ${poke_name}" width="324" height="228" /> | |
<div class="species frame"> | |
${species} Pokémon. Length: ${length.feet}'${length.inches}", Weight: ${weight} | |
</div> | |
<div class="lower-half"> | |
<ul class="attacks"> | |
${attackRowsHTML(attacks)} | |
</ul> | |
<div class="multipliers"> | |
<div class="weakness"> | |
<span>weakness</span> | |
${weakness ? energyHTML(weakness) : ''} | |
</div> | |
<div class="resistance"> | |
<span>resistance</span> | |
${resistance ? energyHTML(resistance) : ''} | |
<span class="resistance-total" | |
>${resistance ? '-30' : ''}</span | |
> | |
</div> | |
<div class="retreat-cost"> | |
<span>retreat cost</span> | |
<div>${energyHTML('Colorless').repeat(retreat)}</div> | |
</div> | |
</div> | |
<p class="description frame">${description}</p> | |
<div class="footer"> | |
<span | |
><a | |
href="https://huggingface.co/minimaxir/ai-generated-pokemon-rudalle" | |
>Illus. Max Woolf</a | |
></span | |
> | |
<span><a href="https://huggingface.co/spaces/launch">${new Date().getFullYear()} Hugging Face</a></span> | |
<span title="Rarity">${rarity}</span> | |
</div> | |
</div> | |
</div>`; | |
}; | |
/* Utility */ | |
const getBasePath = () => { | |
return document.location.origin + document.location.pathname; | |
}; | |
const generateDetails = async () => { | |
const details = await fetch(`${getBasePath()}/details`); | |
return await details.json(); | |
}; | |
const createTask = async (prompt) => { | |
const taskResponse = await fetch(`${getBasePath()}task/create?prompt=${prompt}`); | |
const task = await taskResponse.json(); | |
return task; | |
}; | |
const queueTask = async (task_id) => { | |
const queueResponse = await fetch(`${getBasePath()}task/queue?task_id=${task_id}`); | |
return queueResponse.json(); | |
}; | |
const pollTask = async (task) => { | |
const taskResponse = await fetch(`${getBasePath()}task/poll?task_id=${task.task_id}`); | |
return await taskResponse.json(); | |
}; | |
const longPollTask = async (task, interval = 10_000, max) => { | |
const etaDisplay = document.querySelector('.eta'); | |
task = await pollTask(task); | |
if (task.status === 'completed' || (max && task.poll_count > max)) { | |
return task; | |
} | |
etaDisplay.textContent = Math.round(task.eta); | |
await new Promise((resolve) => setTimeout(resolve, interval)); | |
return await longPollTask(task, interval, max); | |
}; | |
/* DOM */ | |
const generateButton = document.querySelector('button.generate'); | |
const durationTimer = () => { | |
const elapsedDisplay = document.querySelector('.elapsed'); | |
let duration = 0.0; | |
return () => { | |
const startTime = performance.now(); | |
const incrementSeconds = setInterval(() => { | |
duration += 0.1; | |
elapsedDisplay.textContent = duration.toFixed(1); | |
}, 100); | |
const updateDuration = (task) => { | |
if (task?.status == 'completed') { | |
duration = task.completed_at - task.created_at; | |
return; | |
} | |
duration = Number(((performance.now() - startTime) / 1_000).toFixed(1)); | |
}; | |
window.addEventListener('focus', updateDuration); | |
return { | |
cleanup: (completedTask) => { | |
updateDuration(completedTask); | |
clearInterval(incrementSeconds); | |
window.removeEventListener('focus', updateDuration); | |
elapsedDisplay.textContent = duration.toFixed(1); | |
}, | |
}; | |
}; | |
}; | |
const rotateCard = () => { | |
const RANGE = 0.1; | |
const INTERVAL = 13; // ~75 per second | |
let previousTime = 0; | |
// Throttle closure | |
return (card, containerMouseEvent) => { | |
const currentTime = performance.now(); | |
if (currentTime - previousTime > INTERVAL) { | |
previousTime = currentTime; | |
const rect = card.getBoundingClientRect(); | |
const rotateX = (containerMouseEvent.clientY - rect.y - rect.height / 2) * RANGE; | |
const rotateY = -(containerMouseEvent.clientX - rect.x - rect.width / 2) * RANGE; | |
card.style.setProperty('--card-rx', rotateX + 'deg'); | |
card.style.setProperty('--card-ry', rotateY + 'deg'); | |
} | |
}; | |
}; | |
const initialiseCardRotation = (scene) => { | |
const card = document.querySelector('.pokecard'); | |
const mousemoveHandler = rotateCard().bind(null, card); | |
scene.addEventListener('mousemove', mousemoveHandler, true); | |
return mousemoveHandler; | |
}; | |
const setOutput = (mode, state) => { | |
const output = document.querySelector('.output'); | |
output.dataset.mode = mode; | |
output.dataset.state = state; | |
}; | |
const screenshotCard = async () => { | |
const card = document.querySelector('.pokecard'); | |
const imageUrl = await toPng(card, { | |
width: 400, | |
height: 558, | |
backgroundColor: 'transparent', | |
style: { | |
transform: 'none', | |
}, | |
}); | |
return imageUrl; | |
}; | |
/* Initialise */ | |
let generating = false; | |
const nameForm = document.querySelector('form.trainer-name'); | |
const nameInput = document.querySelector('input[name="name"'); | |
const booster = document.querySelector('.booster'); | |
const newGenerationButton = document.querySelector('button.generate-new'); | |
const saveButton = document.querySelector('button.save'); | |
let mousemoveHandlerForPreviousCard; | |
let trainerName; | |
let useTrainerName = true; | |
let pokeName; | |
nameInput.addEventListener('input', (e) => { | |
trainerName = [...e.target.value].filter((char) => char.match(/[\wÀ-ÿ '".,&+#!?:/\\()_-]/g)?.length).join(''); | |
nameInput.value = trainerName; | |
updateCardName(); | |
}); | |
const updateCardName = () => { | |
const cardName = document.querySelector('.pokecard .name'); | |
if (!cardName) { | |
return; | |
} | |
let trainerString = ''; | |
if (trainerName && useTrainerName) { | |
trainerName = [...trainerName].filter((char) => char.match(/[\wÀ-ÿ '".,&+#!?:/\\()_-]/g)?.length).join(''); | |
trainerString = `${trainerName}${trainerName.match(/[sSzZ]$/g)?.length ? "' " : "'s "}`; | |
} | |
cardName.innerText = `${trainerString}${pokeName}`; | |
let nameWidth; | |
let cardWidth = document.querySelector('.pokecard').getBoundingClientRect().width; | |
let scale = 1.01; | |
do { | |
scale -= 0.01; | |
cardName.style.transform = `scaleX(${scale})`; | |
nameWidth = cardName.getBoundingClientRect().width; | |
} while (nameWidth / cardWidth > 0.62); | |
}; | |
const generate = async () => { | |
if (generating) { | |
return; | |
} | |
try { | |
const scene = document.querySelector('.scene'); | |
const cardSlot = scene.querySelector('.card-slot'); | |
const durationDisplay = document.querySelector('.duration'); | |
scene.removeEventListener('mousemove', mousemoveHandlerForPreviousCard, true); | |
cardSlot.innerHTML = ''; | |
generating = true; | |
setOutput('booster', 'generating'); | |
const details = await generateDetails(); | |
pokeName = details.name; | |
const task = await createTask(details.energy_type); | |
document.querySelector('.actions').style.opacity = '1'; | |
durationDisplay.classList.add('displayed'); | |
const timer = durationTimer(durationDisplay); | |
const timerCleanup = timer().cleanup; | |
const longPromises = [queueTask(task.task_id), longPollTask(task)]; | |
const completedTask = await Promise.any(longPromises); | |
setOutput('booster', 'completed'); | |
generating = false; | |
timerCleanup(completedTask); | |
cardSlot.innerHTML = cardHTML(details); | |
updateCardName(); | |
const picture = document.querySelector('img.picture'); | |
picture.src = completedTask.value; | |
mousemoveHandlerForPreviousCard = initialiseCardRotation(scene); | |
setOutput('card', 'completed'); | |
} catch (err) { | |
generating = false; | |
console.error(err); | |
} | |
}; | |
const nameToggle = document.querySelector('button.toggle-name'); | |
nameToggle.addEventListener('click', () => { | |
useTrainerName = !useTrainerName; | |
updateCardName(); | |
if (!useTrainerName) { | |
nameToggle.classList.add('off'); | |
} else { | |
nameToggle.classList.remove('off'); | |
} | |
}); | |
nameForm.addEventListener('submit', (e) => { | |
e.preventDefault(); | |
generate(); | |
}); | |
booster.addEventListener('click', generate); | |
newGenerationButton.addEventListener('click', generate); | |
saveButton.addEventListener('click', async () => { | |
let trainerString = ''; | |
if (trainerName && useTrainerName) { | |
trainerName = [...trainerName].filter((char) => char.match(/[\wÀ-ÿ '".,&+#!?:/\\()_-]/g)?.length).join(''); | |
trainerString = `${trainerName}${trainerName.match(/[sSzZ]$/g)?.length ? "' " : "'s "}`; | |
} | |
const a = document.createElement('a'); | |
a.href = await screenshotCard(); | |
a.download = `${trainerString}${pokeName} - This Pokémon Does Not Exist.png`; | |
a.click(); | |
}); | |