|
"""
|
|
Progressive Image Reveal Mosaic System for Tag Collector Game
|
|
|
|
This module provides a completely different approach to the tag mosaic visualization,
|
|
treating it as a progressive image reveal where each tag discovery unveils portions
|
|
of a template image, with rarer tags revealing more pixels.
|
|
"""
|
|
|
|
import os
|
|
import hashlib
|
|
import streamlit as st
|
|
import numpy as np
|
|
import math
|
|
import io
|
|
import time
|
|
import random
|
|
from PIL import Image, ImageDraw, ImageFilter
|
|
from game_constants import RARITY_LEVELS, ENKEPHALIN_CURRENCY_NAME, ENKEPHALIN_ICON
|
|
|
|
|
|
DEFAULT_TEMPLATES_DIR = "mosaics/templates"
|
|
DEFAULT_MOSAICS_DIR = "mosaics"
|
|
|
|
def ensure_directories():
|
|
"""Ensure all required directories exist"""
|
|
|
|
if not os.path.exists(DEFAULT_MOSAICS_DIR):
|
|
os.makedirs(DEFAULT_MOSAICS_DIR)
|
|
|
|
|
|
if not os.path.exists(DEFAULT_TEMPLATES_DIR):
|
|
os.makedirs(DEFAULT_TEMPLATES_DIR)
|
|
|
|
def initialize_mosaic(mosaic_name="main", total_tags=100):
|
|
"""
|
|
Initialize the appropriate mosaic type based on the template file.
|
|
|
|
Args:
|
|
mosaic_name: Name of the mosaic (used for file paths)
|
|
total_tags: Total number of tags expected for this mosaic
|
|
|
|
Returns:
|
|
An instance of RevealMosaic, AnimatedRevealMosaic, or VideoRevealMosaic
|
|
"""
|
|
|
|
|
|
gif_path = os.path.join(DEFAULT_TEMPLATES_DIR, f"{mosaic_name}_template.gif")
|
|
if os.path.exists(gif_path):
|
|
|
|
try:
|
|
img = Image.open(gif_path)
|
|
is_animated = hasattr(img, 'n_frames') and img.n_frames > 1
|
|
if is_animated:
|
|
print(f"Found animated GIF template: {gif_path}")
|
|
return AnimatedRevealMosaic(
|
|
total_tags=total_tags,
|
|
template_path=gif_path,
|
|
mosaic_name=mosaic_name
|
|
)
|
|
except Exception as e:
|
|
print(f"Error checking animation: {e}")
|
|
|
|
|
|
return RevealMosaic(
|
|
total_tags=total_tags,
|
|
mosaic_name=mosaic_name
|
|
)
|
|
|
|
class RevealMosaic:
|
|
"""Manages the progressive revealing of an image as tags are discovered"""
|
|
|
|
def __init__(self,
|
|
total_tags=100,
|
|
template_path=None,
|
|
mosaic_name="main",
|
|
save_path=None,
|
|
mask_color=(0, 0, 0)):
|
|
"""
|
|
Initialize the reveal mosaic
|
|
|
|
Args:
|
|
total_tags: Total number of tags expected for this mosaic
|
|
template_path: Path to the template image. If None, generated from mosaic_name
|
|
mosaic_name: Name of the mosaic for storage
|
|
save_path: Path to save the mosaic mask. If None, generated from mosaic_name
|
|
mask_color: Color to use for the mask (default: black)
|
|
"""
|
|
|
|
self.total_tags = max(1, total_tags)
|
|
self.mosaic_name = mosaic_name
|
|
self.mask_color = mask_color
|
|
|
|
|
|
ensure_directories()
|
|
|
|
|
|
if save_path is None:
|
|
self.save_path = os.path.join(DEFAULT_MOSAICS_DIR, f"{mosaic_name}_mosaic_mask.png")
|
|
else:
|
|
self.save_path = save_path
|
|
|
|
if template_path is None:
|
|
self.template_path = os.path.join(DEFAULT_TEMPLATES_DIR, f"{mosaic_name}_template.png")
|
|
else:
|
|
self.template_path = template_path
|
|
|
|
|
|
self.processed_tags = set()
|
|
self.revealed_pixels = set()
|
|
self.highlighted_tags = []
|
|
|
|
|
|
self.template_image = self.load_template_image()
|
|
|
|
|
|
self.width, self.height = self.template_image.size
|
|
self.total_pixels = self.width * self.height
|
|
|
|
|
|
self.base_pixels_per_tag = self.total_pixels / self.total_tags
|
|
print(f"Base pixels per tag: {self.base_pixels_per_tag:.1f} (Total: {self.total_pixels} pixels, {self.total_tags} tags)")
|
|
|
|
|
|
self.mask = self.load_or_create_mask()
|
|
|
|
|
|
|
|
self._priority_map = None
|
|
|
|
|
|
self.last_update_time = time.time()
|
|
|
|
|
|
self.needs_update = False
|
|
|
|
|
|
self._cached_image = None
|
|
|
|
self._cache_valid = False
|
|
|
|
@property
|
|
def priority_map(self):
|
|
"""Lazy-load the priority map only when needed"""
|
|
if self._priority_map is None:
|
|
print(f"Generating priority map for {self.mosaic_name}...")
|
|
self._priority_map = self.create_priority_map()
|
|
return self._priority_map
|
|
|
|
def load_template_image(self):
|
|
"""Load the template image or create a default one"""
|
|
if os.path.exists(self.template_path):
|
|
try:
|
|
img = Image.open(self.template_path).convert('RGB')
|
|
print(f"Loaded template image from {self.template_path}")
|
|
return img
|
|
except Exception as e:
|
|
print(f"Error loading template image: {e}")
|
|
|
|
|
|
return self.create_default_template()
|
|
|
|
def load_or_create_mask(self):
|
|
"""Load an existing mask or create a new one"""
|
|
if os.path.exists(self.save_path):
|
|
try:
|
|
mask = Image.open(self.save_path).convert('L')
|
|
|
|
|
|
if mask.size != self.template_image.size:
|
|
mask = mask.resize(self.template_image.size)
|
|
|
|
print(f"Loaded mask from {self.save_path}")
|
|
|
|
|
|
revealed_count = 0
|
|
mask_data = mask.getdata()
|
|
for i, pixel in enumerate(mask_data):
|
|
if pixel == 0:
|
|
x = i % self.width
|
|
y = i // self.width
|
|
self.revealed_pixels.add((x, y))
|
|
revealed_count += 1
|
|
|
|
print(f"Mask has {revealed_count} revealed pixels out of {self.total_pixels}")
|
|
return mask
|
|
except Exception as e:
|
|
print(f"Error loading mask: {e}")
|
|
|
|
|
|
mask = Image.new('L', self.template_image.size, 255)
|
|
return mask
|
|
|
|
def has_new_tags(self, collected_tags):
|
|
"""
|
|
Check if there are new tags that haven't been processed yet.
|
|
|
|
Args:
|
|
collected_tags: Dictionary of {tag_name: info} of collected tags
|
|
|
|
Returns:
|
|
Tuple of (has_new_tags, count_of_new_tags)
|
|
"""
|
|
new_tag_count = 0
|
|
|
|
|
|
for tag, info in collected_tags.items():
|
|
|
|
if info.get("count", 0) <= 0:
|
|
continue
|
|
|
|
|
|
if tag not in self.processed_tags:
|
|
new_tag_count += 1
|
|
|
|
return (new_tag_count > 0, new_tag_count)
|
|
|
|
def update_with_tags(self, collected_tags, metadata=None, force_update=False):
|
|
"""
|
|
Update the mosaic with newly collected tags.
|
|
|
|
Args:
|
|
collected_tags: Dictionary of {tag_name: info} of collected tags
|
|
metadata: Optional metadata (not used in this implementation)
|
|
force_update: Force update even if there are no new tags
|
|
|
|
Returns:
|
|
Number of newly revealed pixels
|
|
"""
|
|
|
|
has_new, new_count = self.has_new_tags(collected_tags)
|
|
if not has_new and not force_update:
|
|
|
|
return 0
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
self.highlighted_tags = []
|
|
|
|
|
|
self.needs_update = False
|
|
total_newly_revealed = 0
|
|
|
|
|
|
|
|
known_tags = set(self.processed_tags)
|
|
all_tags_to_process = []
|
|
|
|
|
|
for tag, info in collected_tags.items():
|
|
|
|
if info.get("count", 0) <= 0:
|
|
continue
|
|
|
|
|
|
if tag not in known_tags:
|
|
|
|
rarity = info.get("rarity", "Canard")
|
|
all_tags_to_process.append((tag, rarity, True))
|
|
|
|
|
|
if not all_tags_to_process and (len(self.processed_tags) == 0 or force_update):
|
|
for tag, info in collected_tags.items():
|
|
if info.get("count", 0) <= 0:
|
|
continue
|
|
|
|
rarity = info.get("rarity", "Canard")
|
|
all_tags_to_process.append((tag, rarity, False))
|
|
|
|
|
|
if not all_tags_to_process:
|
|
return 0
|
|
|
|
|
|
rarity_order = ["Impuritas Civitas", "Star of the City", "Urban Nightmare",
|
|
"Urban Plague", "Urban Legend", "Urban Myth", "Canard"]
|
|
|
|
def get_rarity_rank(tag_tuple):
|
|
_, rarity, _ = tag_tuple
|
|
if rarity in rarity_order:
|
|
return rarity_order.index(rarity)
|
|
return len(rarity_order)
|
|
|
|
all_tags_to_process.sort(key=get_rarity_rank)
|
|
|
|
|
|
for tag, rarity, is_new in all_tags_to_process:
|
|
|
|
newly_revealed = self.reveal_pixels_for_tag(tag, rarity)
|
|
total_newly_revealed += newly_revealed
|
|
|
|
|
|
self.processed_tags.add(tag)
|
|
|
|
|
|
if self.needs_update:
|
|
self.update_mask()
|
|
|
|
self._cache_valid = False
|
|
|
|
|
|
self.last_update_time = time.time()
|
|
|
|
|
|
end_time = time.time()
|
|
print(f"Mosaic update processed {len(all_tags_to_process)} tags, revealed {total_newly_revealed} pixels in {end_time - start_time:.3f}s")
|
|
|
|
return total_newly_revealed
|
|
|
|
def reveal_pixels_for_tag(self, tag, rarity):
|
|
"""
|
|
Reveal pixels for a newly discovered tag.
|
|
|
|
Args:
|
|
tag: The tag name
|
|
rarity: The tag's rarity
|
|
|
|
Returns:
|
|
Number of newly revealed pixels
|
|
"""
|
|
|
|
pixels_to_reveal = self.calculate_pixels_to_reveal(rarity)
|
|
|
|
|
|
newly_revealed = []
|
|
for pixel in self.priority_map:
|
|
if pixel not in self.revealed_pixels:
|
|
newly_revealed.append(pixel)
|
|
self.revealed_pixels.add(pixel)
|
|
if len(newly_revealed) >= pixels_to_reveal:
|
|
break
|
|
|
|
|
|
if not newly_revealed:
|
|
return 0
|
|
|
|
|
|
if newly_revealed:
|
|
|
|
highlight_pixel = random.choice(newly_revealed)
|
|
self.highlighted_tags.append((tag, highlight_pixel[0], highlight_pixel[1], rarity))
|
|
|
|
|
|
self.needs_update = True
|
|
|
|
|
|
return len(newly_revealed)
|
|
|
|
def update_mask(self):
|
|
"""Update and save the mask based on current revealed pixels"""
|
|
|
|
new_mask = Image.new('L', (self.width, self.height), 255)
|
|
|
|
|
|
pixels_updated = 0
|
|
for x, y in self.revealed_pixels:
|
|
if 0 <= x < self.width and 0 <= y < self.height:
|
|
new_mask.putpixel((x, y), 0)
|
|
pixels_updated += 1
|
|
|
|
print(f"Updated mask with {pixels_updated} revealed pixels")
|
|
|
|
|
|
self.mask = new_mask
|
|
try:
|
|
self.mask.save(self.save_path)
|
|
print(f"Saved mask to {self.save_path}")
|
|
except Exception as e:
|
|
print(f"Error saving mask: {e}")
|
|
|
|
|
|
self._cache_valid = False
|
|
|
|
def get_image(self, force_refresh=False):
|
|
"""
|
|
Get the current mosaic image (template with mask applied).
|
|
Uses caching to avoid regenerating the image unless needed.
|
|
|
|
Args:
|
|
force_refresh: Force regeneration of the image even if cached
|
|
|
|
Returns:
|
|
PIL Image of the current state
|
|
"""
|
|
|
|
if force_refresh or not self._cache_valid or self._cached_image is None:
|
|
print(f"Regenerating mosaic image for {self.mosaic_name}")
|
|
|
|
|
|
if os.path.exists(self.save_path):
|
|
try:
|
|
file_mask = Image.open(self.save_path).convert('L')
|
|
if file_mask.size == self.mask.size:
|
|
|
|
diff = 0
|
|
file_data = file_mask.getdata()
|
|
mask_data = self.mask.getdata()
|
|
for i, (f_pixel, m_pixel) in enumerate(zip(file_data, mask_data)):
|
|
if f_pixel != m_pixel:
|
|
diff += 1
|
|
|
|
if diff > 0:
|
|
print(f"Mask file differs from memory by {diff} pixels, reloading")
|
|
self.mask = file_mask
|
|
except Exception as e:
|
|
print(f"Error comparing masks: {e}")
|
|
|
|
|
|
result = self.template_image.copy()
|
|
|
|
|
|
mask_color_img = Image.new('RGB', result.size, self.mask_color)
|
|
|
|
|
|
result.paste(mask_color_img, (0, 0), self.mask)
|
|
|
|
|
|
self._cached_image = result
|
|
self._cache_valid = True
|
|
|
|
return result
|
|
else:
|
|
|
|
return self._cached_image
|
|
|
|
def load_or_create_mask(self):
|
|
"""Load an existing mask or create a new one"""
|
|
if os.path.exists(self.save_path):
|
|
try:
|
|
mask = Image.open(self.save_path).convert('L')
|
|
|
|
|
|
if mask.size != self.template_image.size:
|
|
print(f"Resizing mask from {mask.size} to {self.template_image.size}")
|
|
mask = mask.resize(self.template_image.size)
|
|
|
|
mask.save(self.save_path)
|
|
|
|
print(f"Loaded mask from {self.save_path}")
|
|
|
|
|
|
revealed_count = 0
|
|
mask_data = mask.getdata()
|
|
for i, pixel in enumerate(mask_data):
|
|
if pixel == 0:
|
|
x = i % self.width
|
|
y = i // self.width
|
|
self.revealed_pixels.add((x, y))
|
|
revealed_count += 1
|
|
|
|
print(f"Loaded mask has {revealed_count} revealed pixels out of {self.total_pixels}")
|
|
return mask
|
|
except Exception as e:
|
|
print(f"Error loading mask: {e}")
|
|
|
|
|
|
print(f"Creating new mask for {self.mosaic_name}")
|
|
mask = Image.new('L', self.template_image.size, 255)
|
|
|
|
mask.save(self.save_path)
|
|
return mask
|
|
|
|
def get_stats(self):
|
|
"""
|
|
Get statistics about the mosaic completion.
|
|
|
|
Returns:
|
|
Dictionary with completion statistics
|
|
"""
|
|
|
|
revealed_count = len(self.revealed_pixels)
|
|
completion_percentage = min(100, (revealed_count / self.total_pixels) * 100)
|
|
|
|
return {
|
|
"revealed_pixels": revealed_count,
|
|
"total_pixels": self.total_pixels,
|
|
"completion_percentage": completion_percentage,
|
|
"completion_pattern": get_completion_pattern(completion_percentage),
|
|
"newly_highlighted": len(self.highlighted_tags),
|
|
"has_new_tags": len(self.highlighted_tags) > 0
|
|
}
|
|
|
|
def calculate_pixels_to_reveal(self, rarity):
|
|
"""
|
|
Calculate how many pixels to reveal based on tag rarity and total tags.
|
|
|
|
Args:
|
|
rarity: The rarity of the discovered tag
|
|
|
|
Returns:
|
|
Number of pixels to reveal
|
|
"""
|
|
|
|
|
|
total_tags_expected = max(1, self.total_tags)
|
|
|
|
|
|
base_pixels_per_tag = self.total_pixels / total_tags_expected
|
|
|
|
|
|
rarity_multiplier = 1.0
|
|
if rarity == "Canard":
|
|
rarity_multiplier = 0.5
|
|
elif rarity == "Urban Myth":
|
|
rarity_multiplier = 0.8
|
|
elif rarity == "Urban Legend":
|
|
rarity_multiplier = 1.0
|
|
elif rarity == "Urban Plague":
|
|
rarity_multiplier = 1.5
|
|
elif rarity == "Urban Nightmare":
|
|
rarity_multiplier = 2.0
|
|
elif rarity == "Star of the City":
|
|
rarity_multiplier = 3.0
|
|
elif rarity == "Impuritas Civitas":
|
|
rarity_multiplier = 5.0
|
|
|
|
|
|
pixels_to_reveal = max(100, int(base_pixels_per_tag * rarity_multiplier))
|
|
|
|
|
|
max_at_once = min(100000, int(self.total_pixels * 0.1))
|
|
pixels_to_reveal = min(pixels_to_reveal, max_at_once)
|
|
|
|
|
|
unrevealed_pixels = self.total_pixels - len(self.revealed_pixels)
|
|
pixels_to_reveal = min(pixels_to_reveal, unrevealed_pixels)
|
|
|
|
return pixels_to_reveal
|
|
|
|
def create_priority_map(self):
|
|
"""
|
|
Create a priority map for pixel reveal order.
|
|
|
|
Returns:
|
|
List of pixel coordinates in priority order (highest to lowest)
|
|
"""
|
|
|
|
gray_img = self.template_image.convert('L')
|
|
|
|
|
|
brightness_map = {}
|
|
width, height = gray_img.size
|
|
|
|
|
|
center_x, center_y = width // 2, height // 2
|
|
max_dist = math.sqrt(center_x**2 + center_y**2)
|
|
|
|
|
|
|
|
|
|
for y in range(height):
|
|
for x in range(width):
|
|
|
|
dx, dy = x - center_x, y - center_y
|
|
distance = math.sqrt(dx**2 + dy**2)
|
|
distance_factor = 1.0 - (distance / max_dist)
|
|
|
|
|
|
brightness = gray_img.getpixel((x, y)) / 255.0
|
|
|
|
|
|
|
|
edge_factor = 0.0
|
|
if x > 0 and x < width-1 and y > 0 and y < height-1:
|
|
|
|
neighbors = [
|
|
gray_img.getpixel((x-1, y)),
|
|
gray_img.getpixel((x+1, y)),
|
|
gray_img.getpixel((x, y-1)),
|
|
gray_img.getpixel((x, y+1))
|
|
]
|
|
current = gray_img.getpixel((x, y))
|
|
|
|
diff_sum = sum(abs(current - n) for n in neighbors)
|
|
edge_factor = min(1.0, diff_sum / (4 * 255.0))
|
|
|
|
|
|
|
|
priority = (distance_factor * 0.4) + (brightness * 0.4) + (edge_factor * 0.2)
|
|
|
|
|
|
random_factor = random.random() * 0.01
|
|
brightness_map[(x, y)] = priority + random_factor
|
|
|
|
|
|
sorted_pixels = sorted(brightness_map.items(), key=lambda x: x[1], reverse=True)
|
|
|
|
|
|
return [pixel for pixel, _ in sorted_pixels]
|
|
|
|
def create_default_template(self, width=1024, height=1024):
|
|
"""Create a default colorful template image"""
|
|
|
|
img = Image.new('RGB', (width, height), (0, 0, 0))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
|
|
center_x, center_y = width // 2, height // 2
|
|
max_radius = min(width, height) // 2 - 10
|
|
|
|
|
|
for y in range(height):
|
|
for x in range(width):
|
|
|
|
dx, dy = x - center_x, y - center_y
|
|
distance = math.sqrt(dx*dx + dy*dy)
|
|
|
|
|
|
angle = math.atan2(dy, dx)
|
|
|
|
|
|
norm_distance = min(1.0, distance / max_radius)
|
|
|
|
|
|
|
|
hue = (math.degrees(angle) % 360) / 360.0
|
|
saturation = 0.7 - 0.3 * norm_distance
|
|
value = 0.2 + 0.3 * (1 - norm_distance)
|
|
|
|
|
|
h = hue * 6
|
|
i = int(h)
|
|
f = h - i
|
|
p = value * (1 - saturation)
|
|
q = value * (1 - saturation * f)
|
|
t = value * (1 - saturation * (1 - f))
|
|
|
|
if i == 0:
|
|
r, g, b = value, t, p
|
|
elif i == 1:
|
|
r, g, b = q, value, p
|
|
elif i == 2:
|
|
r, g, b = p, value, t
|
|
elif i == 3:
|
|
r, g, b = p, q, value
|
|
elif i == 4:
|
|
r, g, b = t, p, value
|
|
else:
|
|
r, g, b = value, p, q
|
|
|
|
r, g, b = int(r * 255), int(g * 255), int(b * 255)
|
|
|
|
|
|
if distance < max_radius * 0.2:
|
|
|
|
brightness = 1.0 - (distance / (max_radius * 0.2))
|
|
r = min(255, r + int(brightness * (255 - r)))
|
|
g = min(255, g + int(brightness * (255 - g)))
|
|
b = min(255, b + int(brightness * (255 - b)))
|
|
|
|
|
|
img.putpixel((x, y), (r, g, b))
|
|
|
|
|
|
for r in range(50, 0, -1):
|
|
brightness = 1.0 - (r / 50)
|
|
color = (
|
|
min(255, int(200 + brightness * 55)),
|
|
min(255, int(150 + brightness * 105)),
|
|
min(255, int(100 + brightness * 155))
|
|
)
|
|
draw.ellipse((center_x - r, center_y - r, center_x + r, center_y + r), fill=color)
|
|
|
|
|
|
img = img.filter(ImageFilter.GaussianBlur(radius=1.5))
|
|
|
|
|
|
img.save(self.template_path)
|
|
print(f"Created default template at {self.template_path}")
|
|
return img
|
|
|
|
def get_completion_pattern(completion_percentage):
|
|
"""
|
|
Get a description of the completion pattern based on percentage.
|
|
|
|
Args:
|
|
completion_percentage: Percentage of completion (0-100)
|
|
|
|
Returns:
|
|
String description of what's visible
|
|
"""
|
|
if completion_percentage < 1:
|
|
return "the first glimpses of a hidden image"
|
|
elif completion_percentage < 5:
|
|
return "emerging fragments of a mysterious picture"
|
|
elif completion_percentage < 15:
|
|
return "a partial revelation of the concealed artwork"
|
|
elif completion_percentage < 30:
|
|
return "a quarter of the image taking shape"
|
|
elif completion_percentage < 50:
|
|
return "half of the picture becoming clear"
|
|
elif completion_percentage < 75:
|
|
return "most of the image revealed"
|
|
elif completion_percentage < 95:
|
|
return "nearly complete image with just a few hidden details"
|
|
else:
|
|
return "fully revealed artwork"
|
|
|
|
class AnimatedRevealMosaic(RevealMosaic):
|
|
"""Manages the progressive revealing of an animated GIF or video as tags are discovered"""
|
|
|
|
def __init__(self,
|
|
total_tags=100,
|
|
template_path=None,
|
|
mosaic_name="main",
|
|
save_path=None,
|
|
mask_color=(0, 0, 0)):
|
|
"""Initialize with support for animated GIFs"""
|
|
|
|
self.is_animated = False
|
|
self.frames = []
|
|
self.frame_durations = []
|
|
|
|
|
|
super().__init__(
|
|
total_tags=total_tags,
|
|
template_path=template_path,
|
|
mosaic_name=mosaic_name,
|
|
save_path=save_path,
|
|
mask_color=mask_color
|
|
)
|
|
|
|
def load_template_image(self):
|
|
"""Load the template image with support for animated GIFs"""
|
|
if os.path.exists(self.template_path):
|
|
try:
|
|
|
|
img = Image.open(self.template_path)
|
|
|
|
|
|
try:
|
|
|
|
self.is_animated = hasattr(img, 'n_frames') and img.n_frames > 1
|
|
|
|
if self.is_animated:
|
|
print(f"Loading animated GIF with {img.n_frames} frames")
|
|
|
|
|
|
self.frames = []
|
|
self.frame_durations = []
|
|
|
|
for frame_idx in range(img.n_frames):
|
|
img.seek(frame_idx)
|
|
|
|
self.frame_durations.append(img.info.get('duration', 100))
|
|
|
|
self.frames.append(img.convert('RGB').copy())
|
|
|
|
|
|
return self.frames[0]
|
|
else:
|
|
|
|
print(f"Loaded static template image from {self.template_path}")
|
|
return img.convert('RGB')
|
|
|
|
except Exception as e:
|
|
print(f"Error processing animation: {e}")
|
|
|
|
return img.convert('RGB')
|
|
|
|
except Exception as e:
|
|
print(f"Error loading template image: {e}")
|
|
|
|
|
|
return self.create_default_template()
|
|
|
|
def get_image(self, force_refresh=False):
|
|
"""
|
|
Get the current mosaic image with support for animation.
|
|
|
|
Args:
|
|
force_refresh: Force regeneration of the image even if cached
|
|
|
|
Returns:
|
|
PIL Image or list of PIL Images for animated GIFs
|
|
"""
|
|
|
|
if not self.is_animated or not self.frames:
|
|
return super().get_image(force_refresh)
|
|
|
|
|
|
if force_refresh or not self._cache_valid or self._cached_image is None:
|
|
print(f"Regenerating animated mosaic image for {self.mosaic_name}")
|
|
|
|
|
|
masked_frames = []
|
|
|
|
|
|
mask_color_img = Image.new('RGB', self.frames[0].size, self.mask_color)
|
|
|
|
|
|
for frame in self.frames:
|
|
|
|
result_frame = frame.copy()
|
|
|
|
|
|
result_frame.paste(mask_color_img, (0, 0), self.mask)
|
|
|
|
|
|
masked_frames.append(result_frame)
|
|
|
|
|
|
self._cached_image = masked_frames
|
|
self._cache_valid = True
|
|
|
|
return masked_frames
|
|
else:
|
|
|
|
return self._cached_image
|
|
|
|
def display_tag_mosaic():
|
|
"""Display the tag mosaic in the game UI with progressive image reveal"""
|
|
import streamlit as st
|
|
|
|
|
|
with st.container():
|
|
st.subheader("๐งฉ Tag Collection Mosaic")
|
|
|
|
|
|
st.info("โณ Note: Initial loading or switching templates may take some time for high-resolution images due to pixel processing.")
|
|
|
|
|
|
with st.expander("Mosaic Settings", expanded=False):
|
|
|
|
uploaded_file = st.file_uploader("Upload a custom template image", type=["png", "jpg", "jpeg", "gif"])
|
|
if uploaded_file is not None:
|
|
try:
|
|
|
|
image = Image.open(uploaded_file)
|
|
|
|
|
|
is_animated = hasattr(image, 'n_frames') and image.n_frames > 1
|
|
|
|
if is_animated:
|
|
st.info(f"Animated GIF detected with {image.n_frames} frames. Processing may take longer.")
|
|
elif image.width * image.height > 2000000:
|
|
st.warning(f"Large image detected ({image.width}x{image.height}). Processing may take longer.")
|
|
|
|
|
|
ensure_directories()
|
|
|
|
template_path = os.path.join(DEFAULT_TEMPLATES_DIR, "main_template.gif" if is_animated else "main_template.png")
|
|
image.save(template_path)
|
|
st.success("Template updated! Tags will now progressively reveal this image.")
|
|
|
|
mask_path = os.path.join(DEFAULT_MOSAICS_DIR, "main_mosaic_mask.png")
|
|
if os.path.exists(mask_path):
|
|
os.remove(mask_path)
|
|
|
|
if 'tag_mosaic' in st.session_state:
|
|
del st.session_state.tag_mosaic
|
|
except Exception as e:
|
|
st.error(f"Error processing image: {e}")
|
|
|
|
|
|
if 'tag_mosaic' not in st.session_state:
|
|
|
|
total_tags = 70527
|
|
try:
|
|
if hasattr(st.session_state, 'model') and hasattr(st.session_state.model, 'dataset'):
|
|
if hasattr(st.session_state.model.dataset, 'tag_to_idx'):
|
|
total_tags = len(st.session_state.model.dataset.tag_to_idx)
|
|
except Exception as e:
|
|
print(f"Error getting tag count from metadata: {e}")
|
|
|
|
|
|
template_path = os.path.join(DEFAULT_TEMPLATES_DIR, "main_template.gif")
|
|
if os.path.exists(template_path):
|
|
|
|
try:
|
|
img = Image.open(template_path)
|
|
is_animated = hasattr(img, 'n_frames') and img.n_frames > 1
|
|
if is_animated:
|
|
|
|
st.session_state.tag_mosaic = AnimatedRevealMosaic(
|
|
total_tags=total_tags,
|
|
template_path=template_path,
|
|
mosaic_name="main"
|
|
)
|
|
print("Using AnimatedRevealMosaic")
|
|
else:
|
|
|
|
st.session_state.tag_mosaic = RevealMosaic(
|
|
total_tags=total_tags,
|
|
mosaic_name="main"
|
|
)
|
|
except Exception as e:
|
|
print(f"Error checking animation: {e}")
|
|
|
|
st.session_state.tag_mosaic = RevealMosaic(
|
|
total_tags=total_tags,
|
|
mosaic_name="main"
|
|
)
|
|
else:
|
|
|
|
st.session_state.tag_mosaic = RevealMosaic(
|
|
total_tags=total_tags,
|
|
mosaic_name="main"
|
|
)
|
|
|
|
|
|
mosaic = st.session_state.tag_mosaic
|
|
|
|
|
|
if not hasattr(mosaic, 'processed_tags'):
|
|
mosaic.processed_tags = set()
|
|
|
|
|
|
if hasattr(st.session_state, 'collected_tags'):
|
|
|
|
from series_mosaics import display_milestone_tracker
|
|
display_milestone_tracker("main", st.session_state.collected_tags, mosaic.total_tags)
|
|
|
|
|
|
update_requested = st.button("๐ Update Mosaic")
|
|
|
|
|
|
if not update_requested:
|
|
st.info("Click the 'Update Mosaic' button to process new tag discoveries and update the image.")
|
|
|
|
|
|
newly_revealed = 0
|
|
if update_requested and hasattr(st.session_state, 'collected_tags'):
|
|
|
|
with st.spinner("Processing tag discoveries and updating mosaic..."):
|
|
|
|
metadata = st.session_state.model.dataset if hasattr(st.session_state, 'model') else None
|
|
newly_revealed = mosaic.update_with_tags(st.session_state.collected_tags, metadata, force_update=True)
|
|
|
|
|
|
from series_mosaics import check_and_award_milestone_rewards
|
|
|
|
|
|
total_main_tags = mosaic.total_tags
|
|
milestone, reward = check_and_award_milestone_rewards("main", st.session_state.collected_tags, total_main_tags)
|
|
|
|
|
|
if milestone is not None:
|
|
|
|
st.balloons()
|
|
st.success(f"๐ MILESTONE ACHIEVED! {milestone}% Completion of Main Collection!")
|
|
st.success(f"Rewarded with {reward} {ENKEPHALIN_ICON} {ENKEPHALIN_CURRENCY_NAME}!")
|
|
|
|
|
|
st.rerun()
|
|
elif newly_revealed > 0:
|
|
st.success(f"Successfully updated! Revealed {newly_revealed} new pixels.")
|
|
else:
|
|
st.info("No new pixels to reveal.")
|
|
|
|
|
|
stats = mosaic.get_stats()
|
|
|
|
|
|
col1, col2 = st.columns(2)
|
|
with col1:
|
|
st.write(f"**Completion:** {stats['completion_percentage']:.2f}%")
|
|
st.write(f"**Pixels Revealed:** {stats['revealed_pixels']} / {stats['total_pixels']}")
|
|
with col2:
|
|
st.write(f"**Status:** {stats['completion_pattern']}")
|
|
if newly_revealed > 0:
|
|
st.write(f"**Newly Revealed:** {newly_revealed} pixels")
|
|
|
|
|
|
mosaic_img = mosaic.get_image()
|
|
|
|
|
|
if hasattr(mosaic, 'is_animated') and mosaic.is_animated and isinstance(mosaic_img, list):
|
|
|
|
img_bytes = io.BytesIO()
|
|
|
|
|
|
mosaic_img[0].save(
|
|
img_bytes,
|
|
format='GIF',
|
|
save_all=True,
|
|
append_images=mosaic_img[1:],
|
|
duration=mosaic.frame_durations,
|
|
loop=0
|
|
)
|
|
|
|
img_bytes.seek(0)
|
|
st.image(img_bytes, caption="Your Tag Collection Mosaic - Each discovery reveals more of the hidden image",
|
|
use_container_width=True)
|
|
else:
|
|
|
|
img_bytes = io.BytesIO()
|
|
mosaic_img.save(img_bytes, format='PNG')
|
|
img_bytes.seek(0)
|
|
|
|
st.image(img_bytes, caption="Your Tag Collection Mosaic - Each discovery reveals more of the hidden image",
|
|
use_container_width=True)
|
|
|
|
|
|
st.write("**Rarity Legend:**")
|
|
cols = st.columns(len(RARITY_LEVELS))
|
|
for i, (rarity, info) in enumerate(RARITY_LEVELS.items()):
|
|
with cols[i]:
|
|
st.markdown(
|
|
f"<div style='background-color:{info['color']};height:20px;width:20px;display:inline-block;margin-right:5px;'></div> {rarity}",
|
|
unsafe_allow_html=True
|
|
)
|
|
|
|
|
|
if mosaic.highlighted_tags:
|
|
with st.expander("Recently Added Tags", expanded=False):
|
|
for tag, _, _, rarity in mosaic.highlighted_tags:
|
|
color = RARITY_LEVELS.get(rarity, {}).get("color", "#AAAAAA")
|
|
st.markdown(
|
|
f"<span style='color:{color};font-weight:bold;'>{tag}</span>",
|
|
unsafe_allow_html=True
|
|
) |