Spaces:
Running
Running
Ron Au
commited on
Commit
·
79743a3
1
Parent(s):
0483b0c
refactor: V3
Browse files- refactor(inference): Move operations to notebook
- feat(backend): Remove queuing system
- feat(backend): Add rate-limited card pulling
- feat(endpoints): Merge details and image reqs
- perf(images): Save to JPG instead of PNG
- perf(images): Batch generate images
- refactor(logging): Improve details and order
- refactor(rand_attack): Improve readability
- refactor(rand_type): Rename to rand_energy
- refactor(energy types): Use consistent lowercasing
- refactor(energy types): Order alphabetically
- feat(ui): Remove ETA display
- fix(animation): Fix card showing below booster
- feat(input): Add submit button
- .gitattributes +1 -0
- app.py +20 -108
- datasets/pregenerated_pokemon.h5 +3 -0
- lists/names/{Colorless.json → colorless.json} +0 -0
- lists/names/{Darkness.json → darkness.json} +0 -0
- lists/names/{Dragon.json → dragon.json} +0 -0
- lists/names/{Fairy.json → fairy.json} +0 -0
- lists/names/{Fighting.json → fighting.json} +0 -0
- lists/names/{Fire.json → fire.json} +0 -0
- lists/names/{Grass.json → grass.json} +0 -0
- lists/names/{Lightning.json → lightning.json} +0 -0
- lists/names/{Metal.json → metal.json} +0 -0
- lists/names/{Psychic.json → psychic.json} +0 -0
- lists/names/{Water.json → water.json} +0 -0
- modules/dataset.py +39 -0
- modules/details.py +100 -48
- modules/inference.py +0 -64
- notebooks/populate_dataset.ipynb +0 -0
- static/index.html +9 -9
- static/js/card-html.js +14 -14
- static/js/dom-manipulation.js +1 -38
- static/js/index.js +17 -38
- static/js/network.js +0 -59
- static/style.css +81 -78
.gitattributes
CHANGED
|
@@ -25,3 +25,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 25 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 26 |
*.zstandard filter=lfs diff=lfs merge=lfs -text
|
| 27 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 25 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 26 |
*.zstandard filter=lfs diff=lfs merge=lfs -text
|
| 27 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
datasets/pregenerated_pokemon.h5 filter=lfs diff=lfs merge=lfs -text
|
app.py
CHANGED
|
@@ -1,131 +1,43 @@
|
|
| 1 |
-
from
|
| 2 |
-
from
|
| 3 |
|
| 4 |
-
from fastapi import
|
| 5 |
from fastapi.staticfiles import StaticFiles
|
| 6 |
from fastapi.responses import FileResponse
|
| 7 |
-
from pydantic import BaseModel
|
| 8 |
|
| 9 |
-
from modules.details import rand_details
|
| 10 |
-
from modules.
|
| 11 |
|
| 12 |
app = FastAPI(docs_url=None, redoc_url=None)
|
| 13 |
|
| 14 |
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class NewTask(BaseModel):
|
| 20 |
-
prompt = "покемон"
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def get_place_in_queue(task_id):
|
| 24 |
-
queued_tasks = list(task for task in tasks.values()
|
| 25 |
-
if task["status"] == "queued" or task["status"] == "processing")
|
| 26 |
-
|
| 27 |
-
queued_tasks.sort(key=lambda task: task["created_at"])
|
| 28 |
-
|
| 29 |
-
queued_task_ids = list(task["task_id"] for task in queued_tasks)
|
| 30 |
-
|
| 31 |
-
try:
|
| 32 |
-
return queued_task_ids.index(task_id) + 1
|
| 33 |
-
except:
|
| 34 |
-
return 0
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def calculate_eta(task_id):
|
| 38 |
-
total_durations = list(task["completed_at"] - task["started_at"]
|
| 39 |
-
for task in tasks.values() if "completed_at" in task and task["status"] == "completed")
|
| 40 |
-
|
| 41 |
-
initial_place_in_queue = tasks[task_id]["initial_place_in_queue"]
|
| 42 |
-
|
| 43 |
-
if len(total_durations):
|
| 44 |
-
eta = initial_place_in_queue * mean(total_durations)
|
| 45 |
-
else:
|
| 46 |
-
eta = initial_place_in_queue * 35
|
| 47 |
-
|
| 48 |
-
return round(eta, 1)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def next_task(task_id):
|
| 52 |
-
tasks[task_id]["completed_at"] = time()
|
| 53 |
-
|
| 54 |
-
queued_tasks = list(task for task in tasks.values() if task["status"] == "queued")
|
| 55 |
-
|
| 56 |
-
if queued_tasks:
|
| 57 |
-
print(f"{task_id} {tasks[task_id]['status']}. Task/s remaining: {len(queued_tasks)}")
|
| 58 |
-
process_task(queued_tasks[0]["task_id"])
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def process_task(task_id):
|
| 62 |
-
if 'processing' in list(task['status'] for task in tasks.values()):
|
| 63 |
-
return
|
| 64 |
-
|
| 65 |
-
if tasks[task_id]["last_poll"] and time() - tasks[task_id]["last_poll"] > 30:
|
| 66 |
-
tasks[task_id]["status"] = "abandoned"
|
| 67 |
-
next_task(task_id)
|
| 68 |
-
|
| 69 |
-
tasks[task_id]["status"] = "processing"
|
| 70 |
-
tasks[task_id]["started_at"] = time()
|
| 71 |
-
print(f"Processing {task_id}")
|
| 72 |
-
|
| 73 |
-
try:
|
| 74 |
-
tasks[task_id]["value"] = generate_image(tasks[task_id]["prompt"])
|
| 75 |
-
except Exception as ex:
|
| 76 |
-
tasks[task_id]["status"] = "failed"
|
| 77 |
-
tasks[task_id]["error"] = repr(ex)
|
| 78 |
-
else:
|
| 79 |
-
tasks[task_id]["status"] = "completed"
|
| 80 |
-
finally:
|
| 81 |
-
next_task(task_id)
|
| 82 |
|
| 83 |
|
| 84 |
@app.head('/')
|
| 85 |
@app.get('/')
|
| 86 |
-
def index():
|
| 87 |
return FileResponse(path="static/index.html", media_type="text/html")
|
| 88 |
|
| 89 |
|
| 90 |
-
@app.get('/
|
| 91 |
-
def
|
| 92 |
-
|
| 93 |
-
|
| 94 |
|
| 95 |
-
|
| 96 |
-
def create_task(background_tasks: BackgroundTasks, new_task: NewTask):
|
| 97 |
-
created_at = time()
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
"task_id": task_id,
|
| 103 |
-
"status": "queued",
|
| 104 |
-
"eta": None,
|
| 105 |
-
"created_at": created_at,
|
| 106 |
-
"started_at": None,
|
| 107 |
-
"completed_at": None,
|
| 108 |
-
"last_poll": None,
|
| 109 |
-
"poll_count": 0,
|
| 110 |
-
"initial_place_in_queue": None,
|
| 111 |
-
"place_in_queue": None,
|
| 112 |
-
"prompt": new_task.prompt,
|
| 113 |
-
"value": None,
|
| 114 |
}
|
| 115 |
|
| 116 |
-
tasks[task_id]["initial_place_in_queue"] = get_place_in_queue(task_id)
|
| 117 |
-
tasks[task_id]["eta"] = calculate_eta(task_id)
|
| 118 |
-
|
| 119 |
-
background_tasks.add_task(process_task, task_id)
|
| 120 |
-
|
| 121 |
-
return tasks[task_id]
|
| 122 |
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
-
@app.get('/task/poll')
|
| 125 |
-
def poll_task(task_id: str):
|
| 126 |
-
tasks[task_id]["place_in_queue"] = get_place_in_queue(task_id)
|
| 127 |
-
tasks[task_id]["eta"] = calculate_eta(task_id)
|
| 128 |
-
tasks[task_id]["last_poll"] = time()
|
| 129 |
-
tasks[task_id]["poll_count"] += 1
|
| 130 |
|
| 131 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Union
|
| 2 |
+
from time import gmtime, strftime
|
| 3 |
|
| 4 |
+
from fastapi import FastAPI
|
| 5 |
from fastapi.staticfiles import StaticFiles
|
| 6 |
from fastapi.responses import FileResponse
|
|
|
|
| 7 |
|
| 8 |
+
from modules.details import Details, rand_details
|
| 9 |
+
from modules.dataset import get_image, get_stats
|
| 10 |
|
| 11 |
app = FastAPI(docs_url=None, redoc_url=None)
|
| 12 |
|
| 13 |
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 14 |
|
| 15 |
+
card_logs = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
@app.head('/')
|
| 19 |
@app.get('/')
|
| 20 |
+
def index() -> FileResponse:
|
| 21 |
return FileResponse(path="static/index.html", media_type="text/html")
|
| 22 |
|
| 23 |
|
| 24 |
+
@app.get('/new_card')
|
| 25 |
+
def new_card() -> dict[str, Union[Details, str]]:
|
| 26 |
+
card_logs.append(strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()))
|
|
|
|
| 27 |
|
| 28 |
+
details: Details = rand_details()
|
|
|
|
|
|
|
| 29 |
|
| 30 |
+
return {
|
| 31 |
+
"details": details,
|
| 32 |
+
"image": get_image(details["energy_type"]),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
@app.get('/stats')
|
| 37 |
+
def stats() -> dict[str, Union[int, object]]:
|
| 38 |
+
return get_stats() | {"cards_served": len(card_logs)}
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
@app.get('/logs')
|
| 42 |
+
def logs() -> list[str]:
|
| 43 |
+
return card_logs
|
datasets/pregenerated_pokemon.h5
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:69bfb8a58317d91df48c385fb8051805ae7fa75cbe116ffab02c74231fb86e5c
|
| 3 |
+
size 412480704
|
lists/names/{Colorless.json → colorless.json}
RENAMED
|
File without changes
|
lists/names/{Darkness.json → darkness.json}
RENAMED
|
File without changes
|
lists/names/{Dragon.json → dragon.json}
RENAMED
|
File without changes
|
lists/names/{Fairy.json → fairy.json}
RENAMED
|
File without changes
|
lists/names/{Fighting.json → fighting.json}
RENAMED
|
File without changes
|
lists/names/{Fire.json → fire.json}
RENAMED
|
File without changes
|
lists/names/{Grass.json → grass.json}
RENAMED
|
File without changes
|
lists/names/{Lightning.json → lightning.json}
RENAMED
|
File without changes
|
lists/names/{Metal.json → metal.json}
RENAMED
|
File without changes
|
lists/names/{Psychic.json → psychic.json}
RENAMED
|
File without changes
|
lists/names/{Water.json → water.json}
RENAMED
|
File without changes
|
modules/dataset.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from random import choices, randint
|
| 3 |
+
from typing import cast, Optional, TypedDict
|
| 4 |
+
import h5py
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
datasets_dir: str = './datasets'
|
| 8 |
+
datasets_file: str = 'pregenerated_pokemon.h5'
|
| 9 |
+
h5_file: str = os.path.join(datasets_dir, datasets_file)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Stats(TypedDict):
|
| 13 |
+
size_total: int
|
| 14 |
+
size_mb: float
|
| 15 |
+
size_counts: dict[str, int]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def get_stats(h5_file: str = h5_file) -> Stats:
|
| 19 |
+
with h5py.File(h5_file, 'r') as datasets:
|
| 20 |
+
return {
|
| 21 |
+
"size_total": sum(list(datasets[energy].size.item() for energy in datasets.keys())),
|
| 22 |
+
"size_mb": round(os.path.getsize(h5_file) / 1024**2, 1),
|
| 23 |
+
"size_counts": {key: datasets[key].size.item() for key in datasets.keys()},
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
energy_types: list[str] = ['colorless', 'darkness', 'dragon', 'fairy', 'fighting',
|
| 28 |
+
'fire', 'grass', 'lightning', 'metal', 'psychic', 'water']
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def get_image(energy: Optional[str] = None, row: Optional[int] = None) -> str:
|
| 32 |
+
if not energy:
|
| 33 |
+
energy = choices(energy_types)[0]
|
| 34 |
+
|
| 35 |
+
with h5py.File(h5_file, 'r') as datasets:
|
| 36 |
+
if not row:
|
| 37 |
+
row = randint(0, datasets[energy].size - 1)
|
| 38 |
+
|
| 39 |
+
return datasets[energy].asstr()[row][0]
|
modules/details.py
CHANGED
|
@@ -1,8 +1,20 @@
|
|
| 1 |
import random
|
| 2 |
import json
|
|
|
|
| 3 |
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
lists = {}
|
| 7 |
|
| 8 |
for name in list_names:
|
|
@@ -12,45 +24,47 @@ def load_lists(list_names, base_dir="lists"):
|
|
| 12 |
return lists
|
| 13 |
|
| 14 |
|
| 15 |
-
def rand_hp():
|
| 16 |
# Weights from https://bulbapedia.bulbagarden.net/wiki/HP_(TCG)
|
| 17 |
|
| 18 |
-
hp_range = list(range(30, 340 + 1, 10))
|
| 19 |
|
| 20 |
-
weights = [156, 542, 1264, 1727, 1477, 1232, 1008, 640, 436, 515, 469, 279, 188,
|
| 21 |
-
|
| 22 |
|
| 23 |
return random.choices(hp_range, weights)[0]
|
| 24 |
|
| 25 |
|
| 26 |
-
def
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
if can_be_none:
|
| 29 |
return random.choices([random.choices(types)[0], None])[0]
|
| 30 |
else:
|
| 31 |
return random.choices(types)[0]
|
| 32 |
|
| 33 |
|
| 34 |
-
def rand_name(energy_type=
|
| 35 |
-
lists = load_lists([energy_type], 'lists/names')
|
| 36 |
|
| 37 |
-
return random.choices(lists[energy_type])[0]
|
| 38 |
|
| 39 |
|
| 40 |
-
def rand_species(species):
|
| 41 |
-
random_species = random.choices(species)[0]
|
| 42 |
|
| 43 |
return f'{random_species.capitalize()}'
|
| 44 |
|
| 45 |
|
| 46 |
-
def rand_length():
|
| 47 |
# Weights from https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_height
|
| 48 |
|
| 49 |
-
feet_ranges = [
|
| 50 |
-
|
| 51 |
|
| 52 |
-
weights = [30, 220, 230, 176, 130, 109, 63, 27, 17, 17, 5, 5, 6,
|
| 53 |
-
|
| 54 |
|
| 55 |
return {
|
| 56 |
"feet": random.choices(feet_ranges, weights)[0],
|
|
@@ -58,45 +72,65 @@ def rand_length():
|
|
| 58 |
}
|
| 59 |
|
| 60 |
|
| 61 |
-
def rand_weight():
|
| 62 |
# Weights from https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_weight
|
| 63 |
|
| 64 |
-
weight_ranges
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
start, end = random.choices(weight_ranges, weights)[0].values()
|
| 71 |
|
| 72 |
-
random_weight = random.randrange(start, end + 1, 1)
|
| 73 |
|
| 74 |
return f'{random_weight} lbs.'
|
| 75 |
|
| 76 |
|
| 77 |
-
def rand_attack(
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
-
|
| 81 |
-
# so this would loop indefinitely if looking for one
|
| 82 |
|
| 83 |
-
if energy_type
|
| 84 |
-
|
| 85 |
-
random_attack
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
| 89 |
|
| 90 |
random_attack['text'] = random_attack['text'].replace('<name>', name)
|
| 91 |
|
| 92 |
return random_attack
|
| 93 |
|
| 94 |
|
| 95 |
-
def rand_attacks(attacks, name, energy_type
|
| 96 |
-
attack1 = rand_attack(attacks, name, energy_type)
|
| 97 |
|
| 98 |
if n > 1:
|
| 99 |
-
attack2 = rand_attack(attacks, name, energy_type, True)
|
| 100 |
while attack1['text'] == attack2['text']:
|
| 101 |
attack2 = rand_attack(attacks, name, energy_type, True)
|
| 102 |
return [attack1, attack2]
|
|
@@ -104,32 +138,50 @@ def rand_attacks(attacks, name, energy_type=None, n=2):
|
|
| 104 |
return [attack1]
|
| 105 |
|
| 106 |
|
| 107 |
-
def rand_retreat():
|
| 108 |
return random.randrange(0, 4, 1)
|
| 109 |
|
| 110 |
|
| 111 |
-
def rand_description(descriptions):
|
| 112 |
return random.choices(descriptions)[0]
|
| 113 |
|
| 114 |
|
| 115 |
-
def rand_rarity():
|
| 116 |
return random.choices(['●', '◆', '★'], [10, 5, 1])[0]
|
| 117 |
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
return {
|
| 124 |
"name": name,
|
| 125 |
"hp": rand_hp(),
|
| 126 |
"energy_type": energy_type,
|
| 127 |
-
"species": rand_species(lists["species"]),
|
| 128 |
"length": rand_length(),
|
| 129 |
"weight": rand_weight(),
|
| 130 |
-
"attacks": rand_attacks(lists["attacks"], name, energy_type=energy_type),
|
| 131 |
-
"weakness":
|
| 132 |
-
"resistance":
|
| 133 |
"retreat": rand_retreat(),
|
| 134 |
"description": rand_description(lists["descriptions"]),
|
| 135 |
"rarity": rand_rarity(),
|
|
|
|
| 1 |
import random
|
| 2 |
import json
|
| 3 |
+
from typing import cast, Optional, TypedDict, Union
|
| 4 |
|
| 5 |
|
| 6 |
+
class Attack(TypedDict):
|
| 7 |
+
name: str
|
| 8 |
+
cost: list[str]
|
| 9 |
+
convertedEnergyCost: int
|
| 10 |
+
damage: str
|
| 11 |
+
text: str
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
ListCollection = dict[str, Union[list[str], list[Attack]]]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def load_lists(list_names: list[str], base_dir: str = "lists") -> ListCollection:
|
| 18 |
lists = {}
|
| 19 |
|
| 20 |
for name in list_names:
|
|
|
|
| 24 |
return lists
|
| 25 |
|
| 26 |
|
| 27 |
+
def rand_hp() -> int:
|
| 28 |
# Weights from https://bulbapedia.bulbagarden.net/wiki/HP_(TCG)
|
| 29 |
|
| 30 |
+
hp_range: list[int] = list(range(30, 340 + 1, 10))
|
| 31 |
|
| 32 |
+
weights: list[int] = [156, 542, 1264, 1727, 1477, 1232, 1008, 640, 436, 515, 469, 279, 188,
|
| 33 |
+
131, 132, 132, 56, 66, 97, 74, 23, 24, 25, 7, 15, 6, 0, 12, 18, 35, 18, 3]
|
| 34 |
|
| 35 |
return random.choices(hp_range, weights)[0]
|
| 36 |
|
| 37 |
|
| 38 |
+
def rand_energy(can_be_none: bool = False) -> Union[str, None]:
|
| 39 |
+
types: list[str] = ['colorless', 'darkness', 'dragon', 'fairy', 'fighting',
|
| 40 |
+
'fire', 'grass', 'lightning', 'metal', 'psychic', 'water']
|
| 41 |
+
|
| 42 |
if can_be_none:
|
| 43 |
return random.choices([random.choices(types)[0], None])[0]
|
| 44 |
else:
|
| 45 |
return random.choices(types)[0]
|
| 46 |
|
| 47 |
|
| 48 |
+
def rand_name(energy_type: str = cast(str, rand_energy())) -> str:
|
| 49 |
+
lists: ListCollection = load_lists([energy_type], 'lists/names')
|
| 50 |
|
| 51 |
+
return cast(str, random.choices(lists[energy_type])[0])
|
| 52 |
|
| 53 |
|
| 54 |
+
def rand_species(species: list[str]) -> str:
|
| 55 |
+
random_species: str = random.choices(species)[0]
|
| 56 |
|
| 57 |
return f'{random_species.capitalize()}'
|
| 58 |
|
| 59 |
|
| 60 |
+
def rand_length() -> dict[str, int]:
|
| 61 |
# Weights from https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_height
|
| 62 |
|
| 63 |
+
feet_ranges: list[int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16,
|
| 64 |
+
17, 18, 19, 20, 21, 22, 23, 24, 26, 28, 30, 32, 34, 35, 47, 65, 328]
|
| 65 |
|
| 66 |
+
weights: list[int] = [30, 220, 230, 176, 130, 109, 63, 27, 17, 17, 5, 5, 6,
|
| 67 |
+
4, 3, 2, 2, 2, 1, 2, 3, 1, 2, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1]
|
| 68 |
|
| 69 |
return {
|
| 70 |
"feet": random.choices(feet_ranges, weights)[0],
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
|
| 75 |
+
def rand_weight() -> str:
|
| 76 |
# Weights from https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_weight
|
| 77 |
|
| 78 |
+
weight_ranges: list[dict[str, int]] = [
|
| 79 |
+
{"start": 1, "end": 22},
|
| 80 |
+
{"start": 22, "end": 44},
|
| 81 |
+
{"start": 44, "end": 55},
|
| 82 |
+
{"start": 55, "end": 110},
|
| 83 |
+
{"start": 110, "end": 132},
|
| 84 |
+
{"start": 132, "end": 218},
|
| 85 |
+
{"start": 218, "end": 220},
|
| 86 |
+
{"start": 221, "end": 226},
|
| 87 |
+
{"start": 226, "end": 331},
|
| 88 |
+
{"start": 331, "end": 441},
|
| 89 |
+
{"start": 441, "end": 451},
|
| 90 |
+
{"start": 452, "end": 661},
|
| 91 |
+
{"start": 661, "end": 677},
|
| 92 |
+
{"start": 677, "end": 793},
|
| 93 |
+
{"start": 794, "end": 903},
|
| 94 |
+
{"start": 903, "end": 2204}]
|
| 95 |
+
|
| 96 |
+
# 'weights' as in statistical weightings, not physical mass
|
| 97 |
+
weights: list[int] = [271, 145, 53, 204, 57, 122, 1, 11, 57, 28, 7, 34, 4, 17, 5, 31]
|
| 98 |
+
|
| 99 |
+
start: int
|
| 100 |
+
end: int
|
| 101 |
start, end = random.choices(weight_ranges, weights)[0].values()
|
| 102 |
|
| 103 |
+
random_weight: int = random.randrange(start, end + 1, 1)
|
| 104 |
|
| 105 |
return f'{random_weight} lbs.'
|
| 106 |
|
| 107 |
|
| 108 |
+
def rand_attack(
|
| 109 |
+
attacks: list[Attack],
|
| 110 |
+
name: str, energy_type: Optional[str],
|
| 111 |
+
colorless_only_allowed: bool = False) -> Attack:
|
| 112 |
+
random_attack: Attack = random.choices(attacks)[0]
|
| 113 |
|
| 114 |
+
energy_type = energy_type.capitalize() if energy_type else None # Energy is capitalised in the JSON lists
|
|
|
|
| 115 |
|
| 116 |
+
if energy_type and energy_type != 'Dragon': # No attacks use Dragon energy so this would otherwise infinitely loop
|
| 117 |
+
if colorless_only_allowed:
|
| 118 |
+
while energy_type not in random_attack["cost"] and 'colorless' not in random_attack["cost"]:
|
| 119 |
+
random_attack = random.choices(attacks)[0]
|
| 120 |
+
else:
|
| 121 |
+
while energy_type not in random_attack["cost"]:
|
| 122 |
+
random_attack = random.choices(attacks)[0]
|
| 123 |
|
| 124 |
random_attack['text'] = random_attack['text'].replace('<name>', name)
|
| 125 |
|
| 126 |
return random_attack
|
| 127 |
|
| 128 |
|
| 129 |
+
def rand_attacks(attacks: list[Attack], name: str, energy_type: Optional[str], n: int = 2) -> list[Attack]:
|
| 130 |
+
attack1: Attack = rand_attack(attacks, name, energy_type)
|
| 131 |
|
| 132 |
if n > 1:
|
| 133 |
+
attack2: Attack = rand_attack(attacks, name, energy_type, True)
|
| 134 |
while attack1['text'] == attack2['text']:
|
| 135 |
attack2 = rand_attack(attacks, name, energy_type, True)
|
| 136 |
return [attack1, attack2]
|
|
|
|
| 138 |
return [attack1]
|
| 139 |
|
| 140 |
|
| 141 |
+
def rand_retreat() -> int:
|
| 142 |
return random.randrange(0, 4, 1)
|
| 143 |
|
| 144 |
|
| 145 |
+
def rand_description(descriptions) -> str:
|
| 146 |
return random.choices(descriptions)[0]
|
| 147 |
|
| 148 |
|
| 149 |
+
def rand_rarity() -> str:
|
| 150 |
return random.choices(['●', '◆', '★'], [10, 5, 1])[0]
|
| 151 |
|
| 152 |
|
| 153 |
+
lists: ListCollection = load_lists(['attacks', 'descriptions', 'species'])
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class Details(TypedDict):
|
| 157 |
+
name: str
|
| 158 |
+
hp: int
|
| 159 |
+
energy_type: str
|
| 160 |
+
species: str
|
| 161 |
+
length: dict[str, int]
|
| 162 |
+
weight: str
|
| 163 |
+
attacks: list[Attack]
|
| 164 |
+
weakness: Union[str, None]
|
| 165 |
+
resistance: Union[str, None]
|
| 166 |
+
retreat: int
|
| 167 |
+
description: str
|
| 168 |
+
rarity: str
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def rand_details() -> Details:
|
| 172 |
+
energy_type: str = cast(str, rand_energy())
|
| 173 |
+
name: str = rand_name(energy_type)
|
| 174 |
|
| 175 |
return {
|
| 176 |
"name": name,
|
| 177 |
"hp": rand_hp(),
|
| 178 |
"energy_type": energy_type,
|
| 179 |
+
"species": rand_species(cast(list[str], lists["species"])),
|
| 180 |
"length": rand_length(),
|
| 181 |
"weight": rand_weight(),
|
| 182 |
+
"attacks": cast(list[Attack], rand_attacks(cast(list[Attack], lists["attacks"]), name, energy_type=energy_type)),
|
| 183 |
+
"weakness": rand_energy(can_be_none=True),
|
| 184 |
+
"resistance": rand_energy(can_be_none=True),
|
| 185 |
"retreat": rand_retreat(),
|
| 186 |
"description": rand_description(lists["descriptions"]),
|
| 187 |
"rarity": rand_rarity(),
|
modules/inference.py
DELETED
|
@@ -1,64 +0,0 @@
|
|
| 1 |
-
from time import gmtime, strftime
|
| 2 |
-
|
| 3 |
-
print(f'{strftime("%Y-%m-%d %H:%M:%S", gmtime())} Preparing for inference...') # noqa
|
| 4 |
-
|
| 5 |
-
from rudalle.pipelines import generate_images
|
| 6 |
-
from rudalle import get_rudalle_model, get_tokenizer, get_vae
|
| 7 |
-
from huggingface_hub import hf_hub_url, cached_download
|
| 8 |
-
import torch
|
| 9 |
-
from io import BytesIO
|
| 10 |
-
import base64
|
| 11 |
-
|
| 12 |
-
print(f"GPUs available: {torch.cuda.device_count()}")
|
| 13 |
-
print(f"GPU[0] memory: {int(torch.cuda.get_device_properties(0).total_memory / 1048576)}Mib")
|
| 14 |
-
print(f"GPU[0] memory reserved: {int(torch.cuda.memory_reserved(0) / 1048576)}Mib")
|
| 15 |
-
print(f"GPU[0] memory allocated: {int(torch.cuda.memory_allocated(0) / 1048576)}Mib")
|
| 16 |
-
|
| 17 |
-
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 18 |
-
fp16 = torch.cuda.is_available()
|
| 19 |
-
|
| 20 |
-
file_dir = "./models"
|
| 21 |
-
file_name = "pytorch_model.bin"
|
| 22 |
-
config_file_url = hf_hub_url(repo_id="minimaxir/ai-generated-pokemon-rudalle", filename=file_name)
|
| 23 |
-
cached_download(config_file_url, cache_dir=file_dir, force_filename=file_name)
|
| 24 |
-
|
| 25 |
-
model = get_rudalle_model('Malevich', pretrained=False, fp16=fp16, device=device)
|
| 26 |
-
model.load_state_dict(torch.load(f"{file_dir}/{file_name}", map_location=f"{'cuda:0' if torch.cuda.is_available() else 'cpu'}"))
|
| 27 |
-
|
| 28 |
-
vae = get_vae().to(device)
|
| 29 |
-
tokenizer = get_tokenizer()
|
| 30 |
-
|
| 31 |
-
print(f'{strftime("%Y-%m-%d %H:%M:%S", gmtime())} Ready for inference')
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def english_to_russian(english_string):
|
| 35 |
-
word_map = {
|
| 36 |
-
"grass": "Покемон трава",
|
| 37 |
-
"fire": "Покемон огня",
|
| 38 |
-
"water": "Покемон в воду",
|
| 39 |
-
"lightning": "Покемон электрического типа",
|
| 40 |
-
"fighting": "Покемон боевого типа",
|
| 41 |
-
"psychic": "Покемон психического типа",
|
| 42 |
-
"colorless": "Покемон нормального типа",
|
| 43 |
-
"darkness": "Покемон темного типа",
|
| 44 |
-
"metal": "Покемон из стали типа",
|
| 45 |
-
"dragon": "Покемон типа дракона",
|
| 46 |
-
"fairy": "Покемон фея"
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
return word_map[english_string.lower()]
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def generate_image(prompt):
|
| 53 |
-
if prompt.lower() in ['grass', 'fire', 'water', 'lightning', 'fighting', 'psychic', 'colorless', 'darkness',
|
| 54 |
-
'metal', 'dragon', 'fairy']:
|
| 55 |
-
prompt = english_to_russian(prompt)
|
| 56 |
-
|
| 57 |
-
result, _ = generate_images(prompt, tokenizer, model, vae, top_k=2048, images_num=1, top_p=0.995)
|
| 58 |
-
|
| 59 |
-
buffer = BytesIO()
|
| 60 |
-
result[0].save(buffer, format="PNG")
|
| 61 |
-
base64_bytes = base64.b64encode(buffer.getvalue())
|
| 62 |
-
base64_string = base64_bytes.decode("UTF-8")
|
| 63 |
-
|
| 64 |
-
return "data:image/png;base64," + base64_string
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
notebooks/populate_dataset.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/index.html
CHANGED
|
@@ -27,14 +27,15 @@
|
|
| 27 |
<img src="static/bluey.png" alt="AI generated creature" width="80" height="80">
|
| 28 |
</div>
|
| 29 |
<h1>This Pokémon<br />Does Not Exist</h1>
|
| 30 |
-
<label>
|
| 31 |
-
|
| 32 |
-
<
|
| 33 |
-
<input name="name" type="text" placeholder="Ash" maxlength="75" />
|
| 34 |
-
|
| 35 |
-
|
|
|
|
| 36 |
<p>
|
| 37 |
-
Each illustration is <strong>generated with AI</strong> using a <a href="https://rudalle.ru/en/" target="_blank">ruDALL-E</a>
|
| 38 |
model <a href="https://huggingface.co/minimaxir/ai-generated-pokemon-rudalle" target="_blank">fine-tuned by Max Woolf.</a> Over
|
| 39 |
<a href="https://huggingface.co/models" target="_blank">30,000 such models</a> are hosted on Hugging Face for immediate use.</a
|
| 40 |
>
|
|
@@ -47,10 +48,9 @@
|
|
| 47 |
<button class="toggle-name" data-include tabindex="-1">Trainer Name</button>
|
| 48 |
<button class="generate-new" tabindex="-1">New Pokémon</button>
|
| 49 |
</div>
|
| 50 |
-
<div class="duration"><span class="elapsed">0.0</span>s (ETA: <span class="eta">35</span>s)</div>
|
| 51 |
</div>
|
| 52 |
<div class="scene">
|
| 53 |
-
<div class="booster">
|
| 54 |
<div class="foil triangle top left"></div>
|
| 55 |
<div class="foil triangle top right"></div>
|
| 56 |
<div class="foil top flat"></div>
|
|
|
|
| 27 |
<img src="static/bluey.png" alt="AI generated creature" width="80" height="80">
|
| 28 |
</div>
|
| 29 |
<h1>This Pokémon<br />Does Not Exist</h1>
|
| 30 |
+
<label for="name-input">Enter your trainer name</label>
|
| 31 |
+
<form class="name-form" action="">
|
| 32 |
+
<!-- <div class="name-interactive"> -->
|
| 33 |
+
<input id="name-input" name="name" type="text" placeholder="Ash" maxlength="75" />
|
| 34 |
+
<button type="submit">Submit</button>
|
| 35 |
+
<!-- </div> -->
|
| 36 |
+
</form>
|
| 37 |
<p>
|
| 38 |
+
Each illustration is <strong>generated with AI</strong> using a <a href="https://rudalle.ru/en/" rel="noopener" target="_blank">ruDALL-E</a>
|
| 39 |
model <a href="https://huggingface.co/minimaxir/ai-generated-pokemon-rudalle" target="_blank">fine-tuned by Max Woolf.</a> Over
|
| 40 |
<a href="https://huggingface.co/models" target="_blank">30,000 such models</a> are hosted on Hugging Face for immediate use.</a
|
| 41 |
>
|
|
|
|
| 48 |
<button class="toggle-name" data-include tabindex="-1">Trainer Name</button>
|
| 49 |
<button class="generate-new" tabindex="-1">New Pokémon</button>
|
| 50 |
</div>
|
|
|
|
| 51 |
</div>
|
| 52 |
<div class="scene">
|
| 53 |
+
<div class="booster" title="Open booster pack for new card">
|
| 54 |
<div class="foil triangle top left"></div>
|
| 55 |
<div class="foil triangle top right"></div>
|
| 56 |
<div class="foil top flat"></div>
|
static/js/card-html.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
const TYPES = {
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
};
|
| 14 |
|
| 15 |
const energyHTML = (type, types = TYPES) => {
|
| 16 |
-
return `<span title="${type} energy" class="energy ${type.toLowerCase()}">${types[type]}</span>`;
|
| 17 |
};
|
| 18 |
|
| 19 |
const attackCostHTML = (cost) => {
|
|
@@ -78,7 +78,7 @@ export const cardHTML = (details) => {
|
|
| 78 |
const poke_name = details.name; // `name` would be reserved JS word
|
| 79 |
|
| 80 |
return `
|
| 81 |
-
<div class="pokecard ${energy_type.toLowerCase()}"
|
| 82 |
<p class="evolves">Basic Pokémon</p>
|
| 83 |
<header>
|
| 84 |
<h1 class="name">${poke_name}</h1>
|
|
@@ -122,5 +122,5 @@ export const cardHTML = (details) => {
|
|
| 122 |
<span title="Rarity">${rarity}</span>
|
| 123 |
</div>
|
| 124 |
</div>
|
| 125 |
-
|
| 126 |
};
|
|
|
|
| 1 |
const TYPES = {
|
| 2 |
+
colorless: '⭐',
|
| 3 |
+
darkness: '🌑',
|
| 4 |
+
dragon: '🐲',
|
| 5 |
+
fairy: '🧚',
|
| 6 |
+
fighting: '✊',
|
| 7 |
+
fire: '🔥',
|
| 8 |
+
grass: '🍃',
|
| 9 |
+
lightning: '⚡',
|
| 10 |
+
metal: '⚙️',
|
| 11 |
+
psychic: '👁️',
|
| 12 |
+
water: '💧',
|
| 13 |
};
|
| 14 |
|
| 15 |
const energyHTML = (type, types = TYPES) => {
|
| 16 |
+
return `<span title="${type} energy" class="energy ${type.toLowerCase()}">${types[type.toLowerCase()]}</span>`;
|
| 17 |
};
|
| 18 |
|
| 19 |
const attackCostHTML = (cost) => {
|
|
|
|
| 78 |
const poke_name = details.name; // `name` would be reserved JS word
|
| 79 |
|
| 80 |
return `
|
| 81 |
+
<div class="pokecard ${energy_type.toLowerCase()}">
|
| 82 |
<p class="evolves">Basic Pokémon</p>
|
| 83 |
<header>
|
| 84 |
<h1 class="name">${poke_name}</h1>
|
|
|
|
| 122 |
<span title="Rarity">${rarity}</span>
|
| 123 |
</div>
|
| 124 |
</div>
|
| 125 |
+
</div>`;
|
| 126 |
};
|
static/js/dom-manipulation.js
CHANGED
|
@@ -1,42 +1,5 @@
|
|
| 1 |
import { toPng } from 'https://cdn.skypack.dev/html-to-image';
|
| 2 |
|
| 3 |
-
const durationTimer = () => {
|
| 4 |
-
const elapsedDisplay = document.querySelector('.elapsed');
|
| 5 |
-
let duration = 0.0;
|
| 6 |
-
|
| 7 |
-
return () => {
|
| 8 |
-
const startTime = performance.now();
|
| 9 |
-
|
| 10 |
-
const incrementSeconds = setInterval(() => {
|
| 11 |
-
duration += 0.1;
|
| 12 |
-
elapsedDisplay.textContent = duration.toFixed(1);
|
| 13 |
-
}, 100);
|
| 14 |
-
|
| 15 |
-
const updateDuration = (task) => {
|
| 16 |
-
if (task?.status == 'completed') {
|
| 17 |
-
duration = Date.now() / 1_000 - task.created_at;
|
| 18 |
-
return;
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
duration = Number(((performance.now() - startTime) / 1_000).toFixed(1));
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
window.addEventListener('focus', updateDuration);
|
| 25 |
-
|
| 26 |
-
return {
|
| 27 |
-
cleanup: (completedTask) => {
|
| 28 |
-
if (completedTask) {
|
| 29 |
-
updateDuration(completedTask);
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
clearInterval(incrementSeconds);
|
| 33 |
-
window.removeEventListener('focus', updateDuration);
|
| 34 |
-
elapsedDisplay.textContent = duration.toFixed(1);
|
| 35 |
-
},
|
| 36 |
-
};
|
| 37 |
-
};
|
| 38 |
-
};
|
| 39 |
-
|
| 40 |
const updateCardName = (trainerName, pokeName, useTrainerName) => {
|
| 41 |
const cardName = document.querySelector('.pokecard .name');
|
| 42 |
|
|
@@ -127,4 +90,4 @@ const screenshotCard = async () => {
|
|
| 127 |
return imageUrl;
|
| 128 |
};
|
| 129 |
|
| 130 |
-
export {
|
|
|
|
| 1 |
import { toPng } from 'https://cdn.skypack.dev/html-to-image';
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
const updateCardName = (trainerName, pokeName, useTrainerName) => {
|
| 4 |
const cardName = document.querySelector('.pokecard .name');
|
| 5 |
|
|
|
|
| 90 |
return imageUrl;
|
| 91 |
};
|
| 92 |
|
| 93 |
+
export { updateCardName, initialiseCardRotation, setOutput, screenshotCard };
|
static/js/index.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
| 1 |
-
import { generateDetails, createTask, longPollTask } from './network.js';
|
| 2 |
import { cardHTML } from './card-html.js';
|
| 3 |
-
import {
|
| 4 |
-
durationTimer,
|
| 5 |
-
updateCardName,
|
| 6 |
-
initialiseCardRotation,
|
| 7 |
-
setOutput,
|
| 8 |
-
screenshotCard,
|
| 9 |
-
} from './dom-manipulation.js';
|
| 10 |
|
| 11 |
const nameInput = document.querySelector('input[name="name"');
|
| 12 |
const nameToggle = document.querySelector('button.toggle-name');
|
|
@@ -25,60 +18,46 @@ const generate = async () => {
|
|
| 25 |
const scene = document.querySelector('.scene');
|
| 26 |
const cardSlot = scene.querySelector('.card-slot');
|
| 27 |
const actions = document.querySelector('.actions');
|
| 28 |
-
const durationDisplay = actions.querySelector('.duration');
|
| 29 |
-
const etaDisplay = durationDisplay.querySelector('.eta');
|
| 30 |
-
const timer = durationTimer(durationDisplay);
|
| 31 |
-
const timerCleanup = timer().cleanup;
|
| 32 |
|
| 33 |
scene.removeEventListener('mousemove', mousemoveHandlerForPreviousCard, true);
|
| 34 |
cardSlot.innerHTML = '';
|
| 35 |
generating = true;
|
|
|
|
| 36 |
setOutput('booster', 'generating');
|
| 37 |
|
| 38 |
try {
|
| 39 |
-
const details = await generateDetails();
|
| 40 |
-
pokeName = details.name;
|
| 41 |
-
const task = await createTask(details.energy_type);
|
| 42 |
-
|
| 43 |
actions.style.opacity = '1';
|
| 44 |
actions.setAttribute('aria-hidden', 'false');
|
| 45 |
actions.querySelectorAll('button').forEach((button) => button.setAttribute('tabindex', '0'));
|
| 46 |
-
etaDisplay.textContent = Math.round(task.eta);
|
| 47 |
-
durationDisplay.classList.add('displayed');
|
| 48 |
|
| 49 |
-
if (window.innerWidth <=
|
| 50 |
-
|
| 51 |
}
|
| 52 |
|
| 53 |
-
|
| 54 |
-
const interval = 5_000;
|
| 55 |
-
await new Promise((resolve) => setTimeout(resolve, task.eta * 1_000 - interval / 2));
|
| 56 |
-
const completedTask = await longPollTask(task, interval);
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
|
| 61 |
-
|
| 62 |
-
setOutput('booster', 'failed');
|
| 63 |
-
throw new Error(`Task failed: ${completedTask.error}`);
|
| 64 |
-
}
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
cardSlot.innerHTML = cardHTML(details);
|
| 69 |
-
updateCardName(trainerName, pokeName, useTrainerName);
|
| 70 |
-
document.querySelector('img.picture').src = completedTask.value;
|
| 71 |
|
| 72 |
-
|
| 73 |
|
| 74 |
await new Promise((resolve) =>
|
| 75 |
setTimeout(resolve, window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 1_500 : 1_000)
|
| 76 |
);
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
setOutput('card', 'completed');
|
| 79 |
} catch (err) {
|
| 80 |
generating = false;
|
| 81 |
-
timerCleanup();
|
| 82 |
setOutput('booster', 'failed');
|
| 83 |
console.error(err);
|
| 84 |
}
|
|
@@ -92,7 +71,7 @@ nameInput.addEventListener('input', (e) => {
|
|
| 92 |
updateCardName(trainerName, pokeName, useTrainerName);
|
| 93 |
});
|
| 94 |
|
| 95 |
-
document.querySelector('form.
|
| 96 |
e.preventDefault();
|
| 97 |
|
| 98 |
if (document.querySelector('.output').dataset.state === 'completed') {
|
|
|
|
|
|
|
| 1 |
import { cardHTML } from './card-html.js';
|
| 2 |
+
import { updateCardName, initialiseCardRotation, setOutput, screenshotCard } from './dom-manipulation.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
const nameInput = document.querySelector('input[name="name"');
|
| 5 |
const nameToggle = document.querySelector('button.toggle-name');
|
|
|
|
| 18 |
const scene = document.querySelector('.scene');
|
| 19 |
const cardSlot = scene.querySelector('.card-slot');
|
| 20 |
const actions = document.querySelector('.actions');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
scene.removeEventListener('mousemove', mousemoveHandlerForPreviousCard, true);
|
| 23 |
cardSlot.innerHTML = '';
|
| 24 |
generating = true;
|
| 25 |
+
document.querySelector('.scene .booster').removeAttribute('title');
|
| 26 |
setOutput('booster', 'generating');
|
| 27 |
|
| 28 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
actions.style.opacity = '1';
|
| 30 |
actions.setAttribute('aria-hidden', 'false');
|
| 31 |
actions.querySelectorAll('button').forEach((button) => button.setAttribute('tabindex', '0'));
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
if (window.innerWidth <= 920) {
|
| 34 |
+
scene.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 35 |
}
|
| 36 |
|
| 37 |
+
await new Promise((resolve) => setTimeout(resolve, 5_000));
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
const cardResponse = await fetch('/new_card');
|
| 40 |
+
const card = await cardResponse.json();
|
| 41 |
|
| 42 |
+
pokeName = card.details.name;
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
+
generating = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
+
setOutput('booster', 'completed');
|
| 47 |
|
| 48 |
await new Promise((resolve) =>
|
| 49 |
setTimeout(resolve, window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 1_500 : 1_000)
|
| 50 |
);
|
| 51 |
|
| 52 |
+
cardSlot.innerHTML = cardHTML(card.details);
|
| 53 |
+
updateCardName(trainerName, pokeName, useTrainerName);
|
| 54 |
+
document.querySelector('img.picture').src = card.image;
|
| 55 |
+
|
| 56 |
+
mousemoveHandlerForPreviousCard = initialiseCardRotation(scene);
|
| 57 |
+
|
| 58 |
setOutput('card', 'completed');
|
| 59 |
} catch (err) {
|
| 60 |
generating = false;
|
|
|
|
| 61 |
setOutput('booster', 'failed');
|
| 62 |
console.error(err);
|
| 63 |
}
|
|
|
|
| 71 |
updateCardName(trainerName, pokeName, useTrainerName);
|
| 72 |
});
|
| 73 |
|
| 74 |
+
document.querySelector('form.name-form').addEventListener('submit', (e) => {
|
| 75 |
e.preventDefault();
|
| 76 |
|
| 77 |
if (document.querySelector('.output').dataset.state === 'completed') {
|
static/js/network.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Reconcile paths for hf.space resources fetched from hf.co iframe
|
| 3 |
-
*/
|
| 4 |
-
|
| 5 |
-
const pathFor = (path) => {
|
| 6 |
-
const basePath = document.location.origin + document.location.pathname;
|
| 7 |
-
return new URL(path, basePath).href;
|
| 8 |
-
};
|
| 9 |
-
|
| 10 |
-
const generateDetails = async () => {
|
| 11 |
-
const details = await fetch(pathFor('details'));
|
| 12 |
-
return await details.json();
|
| 13 |
-
};
|
| 14 |
-
|
| 15 |
-
const createTask = async (prompt) => {
|
| 16 |
-
const taskResponse = await fetch(pathFor('task/create'), {
|
| 17 |
-
method: 'POST',
|
| 18 |
-
headers: {
|
| 19 |
-
'Content-Type': 'application/json',
|
| 20 |
-
},
|
| 21 |
-
body: JSON.stringify({ prompt }),
|
| 22 |
-
});
|
| 23 |
-
|
| 24 |
-
if (!taskResponse.ok || !taskResponse.headers.get('content-type')?.includes('application/json')) {
|
| 25 |
-
throw new Error(await taskResponse.text());
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
const task = await taskResponse.json();
|
| 29 |
-
|
| 30 |
-
return task;
|
| 31 |
-
};
|
| 32 |
-
|
| 33 |
-
const pollTask = async (task) => {
|
| 34 |
-
const taskResponse = await fetch(pathFor(`task/poll?task_id=${task.task_id}`));
|
| 35 |
-
|
| 36 |
-
if (!taskResponse.ok || !taskResponse.headers.get('content-type')?.includes('application/json')) {
|
| 37 |
-
throw new Error(await taskResponse.text());
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
return await taskResponse.json();
|
| 41 |
-
};
|
| 42 |
-
|
| 43 |
-
const longPollTask = async (task, interval = 5_000, max) => {
|
| 44 |
-
const etaDisplay = document.querySelector('.eta');
|
| 45 |
-
|
| 46 |
-
task = await pollTask(task);
|
| 47 |
-
|
| 48 |
-
if (task.status === 'completed' || task.status === 'failed' || (max && task.poll_count > max)) {
|
| 49 |
-
return task;
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
etaDisplay.textContent = Math.round(task.eta);
|
| 53 |
-
|
| 54 |
-
await new Promise((resolve) => setTimeout(resolve, interval));
|
| 55 |
-
|
| 56 |
-
return await longPollTask(task, interval, max);
|
| 57 |
-
};
|
| 58 |
-
|
| 59 |
-
export { generateDetails, createTask, longPollTask };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/style.css
CHANGED
|
@@ -9,11 +9,6 @@
|
|
| 9 |
--theme-error-border: hsl(355 85% 55%);
|
| 10 |
}
|
| 11 |
|
| 12 |
-
html,
|
| 13 |
-
body {
|
| 14 |
-
overflow: scroll;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
* {
|
| 18 |
transition: outline-offset 0.25s ease-out;
|
| 19 |
outline-style: none;
|
|
@@ -36,11 +31,19 @@ body {
|
|
| 36 |
background-color: gold;
|
| 37 |
}
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
body {
|
| 40 |
-
|
| 41 |
background-color: whitesmoke;
|
| 42 |
background-image: linear-gradient(300deg, var(--theme-highlight), white);
|
| 43 |
font-family: 'Gill Sans', 'Gill Sans Mt', 'sans-serif';
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
main {
|
|
@@ -49,7 +52,8 @@ main {
|
|
| 49 |
grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
|
| 50 |
gap: 1.5rem 0;
|
| 51 |
max-width: 80rem;
|
| 52 |
-
|
|
|
|
| 53 |
margin: 0 auto;
|
| 54 |
}
|
| 55 |
|
|
@@ -69,7 +73,18 @@ main {
|
|
| 69 |
.scene .card-slot {
|
| 70 |
margin-top: 1rem;
|
| 71 |
}
|
|
|
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
|
| 75 |
@media (max-width: 1280px) {
|
|
@@ -148,19 +163,31 @@ section {
|
|
| 148 |
font-weight: 700;
|
| 149 |
}
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
.info input {
|
| 152 |
display: block;
|
| 153 |
-
width:
|
| 154 |
box-sizing: border-box;
|
| 155 |
-
padding: 0.5rem 1rem;
|
| 156 |
-
margin: 0.5rem auto;
|
| 157 |
border: 0.2rem solid hsl(0 0% 70%);
|
| 158 |
-
border-
|
| 159 |
text-align: center;
|
| 160 |
font-size: 1.25rem;
|
| 161 |
-
transition:
|
| 162 |
-
|
| 163 |
-
|
|
|
|
| 164 |
}
|
| 165 |
|
| 166 |
.info input::placeholder {
|
|
@@ -168,9 +195,14 @@ section {
|
|
| 168 |
}
|
| 169 |
|
| 170 |
input:focus {
|
| 171 |
-
border-color:
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
}
|
| 175 |
|
| 176 |
.info p {
|
|
@@ -181,7 +213,8 @@ input:focus {
|
|
| 181 |
line-height: 1.5rem;
|
| 182 |
}
|
| 183 |
|
| 184 |
-
.info a,
|
|
|
|
| 185 |
color: var(--theme-subtext);
|
| 186 |
cursor: pointer;
|
| 187 |
}
|
|
@@ -192,7 +225,7 @@ input:focus {
|
|
| 192 |
display: flex;
|
| 193 |
flex-direction: column;
|
| 194 |
justify-content: space-around;
|
| 195 |
-
height:
|
| 196 |
}
|
| 197 |
|
| 198 |
.output .actions {
|
|
@@ -218,13 +251,16 @@ button {
|
|
| 218 |
font-weight: bold;
|
| 219 |
color: white;
|
| 220 |
transform-origin: bottom;
|
| 221 |
-
|
| 222 |
transition: transform 0.5s ease, box-shadow 0.1s, outline-offset 0.25s ease-out, filter 0.25s ease-out, opacity 0.25s;
|
| 223 |
-
transition: transform 0.5s, opacity 0.5s;
|
| 224 |
whitespace: nowrap;
|
| 225 |
-
box-shadow: 0 0.2rem 0.375rem hsl(158 100% 33% / 60%);
|
| 226 |
filter: saturate(1);
|
| 227 |
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
user-select: none;
|
| 229 |
pointer-events: none;
|
| 230 |
opacity: 0;
|
|
@@ -245,49 +281,12 @@ button.toggle-name.off {
|
|
| 245 |
filter: saturate(0.15);
|
| 246 |
}
|
| 247 |
|
| 248 |
-
.duration {
|
| 249 |
-
visibility: hidden;
|
| 250 |
-
width: max-content;
|
| 251 |
-
padding: 0.35rem 1rem;
|
| 252 |
-
border: 0.1rem solid hsl(90 100% 50% / 25%);
|
| 253 |
-
border-radius: 1rem;
|
| 254 |
-
background-color: var(--theme-highlight);
|
| 255 |
-
text-align: right;
|
| 256 |
-
color: var(--theme-subtext);
|
| 257 |
-
transform: translateY(-25%);
|
| 258 |
-
transition: transform 0.5s, opacity 0.5s;
|
| 259 |
-
opacity: 0;
|
| 260 |
-
}
|
| 261 |
-
|
| 262 |
-
.duration.displayed {
|
| 263 |
-
visibility: visible;
|
| 264 |
-
transform: translateY(0);
|
| 265 |
-
opacity: 1;
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
[data-state="failed"] .duration {
|
| 269 |
-
border-color: var(--theme-error-border);
|
| 270 |
-
background-color: var(--theme-error-bg);
|
| 271 |
-
color: transparent;
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
-
[data-state="failed"] .duration.displayed::after {
|
| 275 |
-
content: 'Try again';
|
| 276 |
-
position: absolute;
|
| 277 |
-
top: 20%;
|
| 278 |
-
left: 0;
|
| 279 |
-
width: 100%;
|
| 280 |
-
text-align: center;
|
| 281 |
-
color: white;
|
| 282 |
-
}
|
| 283 |
-
|
| 284 |
.scene {
|
| 285 |
--scale: 0.9;
|
| 286 |
-
height:
|
| 287 |
box-sizing: border-box;
|
| 288 |
-
margin: 2rem;
|
| 289 |
perspective: 100rem;
|
| 290 |
-
transform-origin: center
|
| 291 |
transform: scale(var(--scale));
|
| 292 |
transition: transform 0.5s ease-out;
|
| 293 |
}
|
|
@@ -584,11 +583,11 @@ img.hf-logo {
|
|
| 584 |
|
| 585 |
@keyframes shrink {
|
| 586 |
from {
|
| 587 |
-
transform: rotateZ(
|
| 588 |
opacity: 1;
|
| 589 |
}
|
| 590 |
to {
|
| 591 |
-
transform: rotateZ(
|
| 592 |
opacity: 0;
|
| 593 |
}
|
| 594 |
}
|
|
@@ -616,6 +615,7 @@ img.hf-logo {
|
|
| 616 |
|
| 617 |
[data-mode='booster'][data-state='completed'] .card-slot {
|
| 618 |
transform: scale(0);
|
|
|
|
| 619 |
}
|
| 620 |
|
| 621 |
[data-mode='booster'][data-state='completed'] .back {
|
|
@@ -628,17 +628,26 @@ img.hf-logo {
|
|
| 628 |
|
| 629 |
[data-mode='card'][data-state='completed'] .card-slot {
|
| 630 |
transform: scale(1);
|
|
|
|
| 631 |
}
|
| 632 |
|
| 633 |
@media (prefers-reduced-motion) {
|
| 634 |
@keyframes pulse {
|
| 635 |
-
from {
|
| 636 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
}
|
| 638 |
|
| 639 |
@keyframes fade {
|
| 640 |
-
from {
|
| 641 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
}
|
| 643 |
|
| 644 |
.card-slot .pokecard {
|
|
@@ -671,12 +680,12 @@ img.hf-logo {
|
|
| 671 |
}
|
| 672 |
}
|
| 673 |
|
| 674 |
-
|
| 675 |
/* Pokémon Card */
|
| 676 |
|
| 677 |
.card-slot {
|
|
|
|
| 678 |
perspective: 100rem;
|
| 679 |
-
transition: transform 0.5s ease-out;
|
| 680 |
}
|
| 681 |
|
| 682 |
.grass {
|
|
@@ -825,13 +834,6 @@ img.hf-logo {
|
|
| 825 |
box-shadow: 0 0.75rem 1.25rem 0 hsl(0 0% 50% / 40%);
|
| 826 |
}
|
| 827 |
|
| 828 |
-
.pokecard[data-displayed='true'] {
|
| 829 |
-
display: flex;
|
| 830 |
-
}
|
| 831 |
-
.pokecard[data-displayed='false'] {
|
| 832 |
-
display: none;
|
| 833 |
-
}
|
| 834 |
-
|
| 835 |
.pokecard .lower-half {
|
| 836 |
display: flex;
|
| 837 |
flex-direction: column;
|
|
@@ -980,11 +982,12 @@ header .energy {
|
|
| 980 |
text-align: center;
|
| 981 |
}
|
| 982 |
|
| 983 |
-
.no-cost .attack-text > span:only-child,
|
|
|
|
| 984 |
width: var(--card-width);
|
| 985 |
margin-left: -2.5rem;
|
| 986 |
}
|
| 987 |
-
.no-damage .attack-text
|
| 988 |
width: var(--card-width);
|
| 989 |
margin-left: -5.5rem;
|
| 990 |
}
|
|
|
|
| 9 |
--theme-error-border: hsl(355 85% 55%);
|
| 10 |
}
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
* {
|
| 13 |
transition: outline-offset 0.25s ease-out;
|
| 14 |
outline-style: none;
|
|
|
|
| 31 |
background-color: gold;
|
| 32 |
}
|
| 33 |
|
| 34 |
+
html {
|
| 35 |
+
display: flex;
|
| 36 |
+
display: grid;
|
| 37 |
+
align-items: center;
|
| 38 |
+
height: 100%;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
body {
|
| 42 |
+
margin: 0;
|
| 43 |
background-color: whitesmoke;
|
| 44 |
background-image: linear-gradient(300deg, var(--theme-highlight), white);
|
| 45 |
font-family: 'Gill Sans', 'Gill Sans Mt', 'sans-serif';
|
| 46 |
+
overflow-x: hidden;
|
| 47 |
}
|
| 48 |
|
| 49 |
main {
|
|
|
|
| 52 |
grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
|
| 53 |
gap: 1.5rem 0;
|
| 54 |
max-width: 80rem;
|
| 55 |
+
height: 100%;
|
| 56 |
+
padding: 0 3rem;
|
| 57 |
margin: 0 auto;
|
| 58 |
}
|
| 59 |
|
|
|
|
| 73 |
.scene .card-slot {
|
| 74 |
margin-top: 1rem;
|
| 75 |
}
|
| 76 |
+
}
|
| 77 |
|
| 78 |
+
@media (max-width: 895px) {
|
| 79 |
+
html {
|
| 80 |
+
height: auto;
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
@media (max-width: 1024px) {
|
| 85 |
+
.output .booster {
|
| 86 |
+
--booster-scale: 0.6;
|
| 87 |
+
}
|
| 88 |
}
|
| 89 |
|
| 90 |
@media (max-width: 1280px) {
|
|
|
|
| 163 |
font-weight: 700;
|
| 164 |
}
|
| 165 |
|
| 166 |
+
.info form {
|
| 167 |
+
display: flex;
|
| 168 |
+
flex-direction: row;
|
| 169 |
+
width: 80%;
|
| 170 |
+
margin: 0.5rem auto;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.info .name-interactive {
|
| 174 |
+
display: flex;
|
| 175 |
+
flex-direction: row;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
.info input {
|
| 179 |
display: block;
|
| 180 |
+
width: 100%;
|
| 181 |
box-sizing: border-box;
|
| 182 |
+
padding: 0.5rem 1rem 0.5rem 5rem;
|
|
|
|
| 183 |
border: 0.2rem solid hsl(0 0% 70%);
|
| 184 |
+
border-right: none;
|
| 185 |
text-align: center;
|
| 186 |
font-size: 1.25rem;
|
| 187 |
+
transition: box-shadow 0.5s ease-out;
|
| 188 |
+
border-top-left-radius: 1rem;
|
| 189 |
+
border-bottom-left-radius: 1rem;
|
| 190 |
+
box-shadow: none;
|
| 191 |
}
|
| 192 |
|
| 193 |
.info input::placeholder {
|
|
|
|
| 195 |
}
|
| 196 |
|
| 197 |
input:focus {
|
| 198 |
+
border-color: var(--theme-secondary);
|
| 199 |
+
box-shadow: 0 0 0.5rem hsl(165 67% 48% / 60%);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
form button {
|
| 203 |
+
height: 2.8125rem;
|
| 204 |
+
border-top-left-radius: 0;
|
| 205 |
+
border-bottom-left-radius: 0;
|
| 206 |
}
|
| 207 |
|
| 208 |
.info p {
|
|
|
|
| 213 |
line-height: 1.5rem;
|
| 214 |
}
|
| 215 |
|
| 216 |
+
.info a,
|
| 217 |
+
info a:is(:hover, :focus, :active, :visited) {
|
| 218 |
color: var(--theme-subtext);
|
| 219 |
cursor: pointer;
|
| 220 |
}
|
|
|
|
| 225 |
display: flex;
|
| 226 |
flex-direction: column;
|
| 227 |
justify-content: space-around;
|
| 228 |
+
height: min-content;
|
| 229 |
}
|
| 230 |
|
| 231 |
.output .actions {
|
|
|
|
| 251 |
font-weight: bold;
|
| 252 |
color: white;
|
| 253 |
transform-origin: bottom;
|
| 254 |
+
|
| 255 |
transition: transform 0.5s ease, box-shadow 0.1s, outline-offset 0.25s ease-out, filter 0.25s ease-out, opacity 0.25s;
|
|
|
|
| 256 |
whitespace: nowrap;
|
|
|
|
| 257 |
filter: saturate(1);
|
| 258 |
cursor: pointer;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.actions button {
|
| 262 |
+
transform: translateY(-25%);
|
| 263 |
+
box-shadow: 0 0.2rem 0.375rem hsl(158 100% 33% / 60%);
|
| 264 |
user-select: none;
|
| 265 |
pointer-events: none;
|
| 266 |
opacity: 0;
|
|
|
|
| 281 |
filter: saturate(0.15);
|
| 282 |
}
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
.scene {
|
| 285 |
--scale: 0.9;
|
| 286 |
+
height: min-content;
|
| 287 |
box-sizing: border-box;
|
|
|
|
| 288 |
perspective: 100rem;
|
| 289 |
+
transform-origin: center;
|
| 290 |
transform: scale(var(--scale));
|
| 291 |
transition: transform 0.5s ease-out;
|
| 292 |
}
|
|
|
|
| 583 |
|
| 584 |
@keyframes shrink {
|
| 585 |
from {
|
| 586 |
+
transform: rotateZ(45deg) scale(var(--booster-scale));
|
| 587 |
opacity: 1;
|
| 588 |
}
|
| 589 |
to {
|
| 590 |
+
transform: rotateZ(270deg) scale(0);
|
| 591 |
opacity: 0;
|
| 592 |
}
|
| 593 |
}
|
|
|
|
| 615 |
|
| 616 |
[data-mode='booster'][data-state='completed'] .card-slot {
|
| 617 |
transform: scale(0);
|
| 618 |
+
opacity: 0;
|
| 619 |
}
|
| 620 |
|
| 621 |
[data-mode='booster'][data-state='completed'] .back {
|
|
|
|
| 628 |
|
| 629 |
[data-mode='card'][data-state='completed'] .card-slot {
|
| 630 |
transform: scale(1);
|
| 631 |
+
opacity: 1;
|
| 632 |
}
|
| 633 |
|
| 634 |
@media (prefers-reduced-motion) {
|
| 635 |
@keyframes pulse {
|
| 636 |
+
from {
|
| 637 |
+
opacity: 1;
|
| 638 |
+
}
|
| 639 |
+
to {
|
| 640 |
+
opacity: 0.6;
|
| 641 |
+
}
|
| 642 |
}
|
| 643 |
|
| 644 |
@keyframes fade {
|
| 645 |
+
from {
|
| 646 |
+
opacity: 1;
|
| 647 |
+
}
|
| 648 |
+
to {
|
| 649 |
+
opacity: 0;
|
| 650 |
+
}
|
| 651 |
}
|
| 652 |
|
| 653 |
.card-slot .pokecard {
|
|
|
|
| 680 |
}
|
| 681 |
}
|
| 682 |
|
|
|
|
| 683 |
/* Pokémon Card */
|
| 684 |
|
| 685 |
.card-slot {
|
| 686 |
+
height: 100%;
|
| 687 |
perspective: 100rem;
|
| 688 |
+
transition: transform 0.5s ease-out, opacity 0.5s ease-in;
|
| 689 |
}
|
| 690 |
|
| 691 |
.grass {
|
|
|
|
| 834 |
box-shadow: 0 0.75rem 1.25rem 0 hsl(0 0% 50% / 40%);
|
| 835 |
}
|
| 836 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
.pokecard .lower-half {
|
| 838 |
display: flex;
|
| 839 |
flex-direction: column;
|
|
|
|
| 982 |
text-align: center;
|
| 983 |
}
|
| 984 |
|
| 985 |
+
.no-cost .attack-text > span:only-child,
|
| 986 |
+
.no-cost.no-damage .attack-text > span:only-child {
|
| 987 |
width: var(--card-width);
|
| 988 |
margin-left: -2.5rem;
|
| 989 |
}
|
| 990 |
+
.no-damage .attack-text > span:only-child {
|
| 991 |
width: var(--card-width);
|
| 992 |
margin-left: -5.5rem;
|
| 993 |
}
|