Ron Au
WIP3: New UI
6f41437
raw
history blame
10.6 kB
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();
});