diff --git a/levelup/generator/styles/default.py b/levelup/generator/styles/default.py index b1a7148..b0885d9 100644 --- a/levelup/generator/styles/default.py +++ b/levelup/generator/styles/default.py @@ -208,19 +208,19 @@ def generate_default_profile( # Define the stats area with a modern glass effect stats_area = ( - 380, # x1 - Start after profile picture + 15, # x1 - Start from left edge with small padding 15, # y1 - Start near top - 1020, # x2 - End near right edge + 1035, # x2 - End near right edge 305 # y2 - Adjusted height ) # Create the stats layer with glass effect stats_layer = Image.new("RGBA", desired_card_size, (0, 0, 0, 0)) - # Create a modern glass morphism effect for stats + # Create a modern glass morphism effect for entire background glass = Image.new("RGBA", desired_card_size, (0, 0, 0, 0)) glass_draw = ImageDraw.Draw(glass) - glass_draw.rounded_rectangle(stats_area, radius=25, fill=(255, 255, 255, 25)) # Lighter, more modern glass + glass_draw.rounded_rectangle(stats_area, radius=25, fill=(0, 0, 0, 100)) # Darker, semi-transparent background # Add a subtle gradient overlay for depth gradient = Image.new("RGBA", desired_card_size, (0, 0, 0, 0)) @@ -262,13 +262,13 @@ def generate_default_profile( # Add subtle text shadow for depth shadow_offset = 2 draw.text( - (stats_area[0] + 20 + shadow_offset, level_y + shadow_offset), + (stats_area[0] + 340 + shadow_offset, level_y + shadow_offset), # Adjusted x position level_text, font=level_font, fill=(0, 0, 0, 80) # Reduced shadow opacity ) draw.text( - (stats_area[0] + 20, level_y), + (stats_area[0] + 340, level_y), # Adjusted x position level_text, font=level_font, fill=user_color @@ -282,7 +282,7 @@ def generate_default_profile( spacing = 45 # Increased spacing # Starting positions - start_x = stats_area[0] + 20 + start_x = stats_area[0] + 340 # Adjusted x position start_y = level_y + 75 # Helper function for stat rendering with improved spacing diff --git a/levelup/generator/styles/modern.py b/levelup/generator/styles/modern.py new file mode 100644 index 0000000..57e4390 --- /dev/null +++ b/levelup/generator/styles/modern.py @@ -0,0 +1,170 @@ +import typing as t +from pathlib import Path +from io import BytesIO +import logging +from wand.image import Image +from wand.drawing import Drawing +from wand.color import Color +from redbot.core.utils.chat_formatting import humanize_number + +try: + from .. import imgtools +except ImportError: + import imgtools + +log = logging.getLogger("red.vrt.levelup.generator.styles.modern") + +def generate_modern_profile( + background_bytes: t.Optional[t.Union[bytes, str]] = None, + avatar_bytes: t.Optional[t.Union[bytes, str]] = None, + username: str = "Spartan117", + status: str = "online", + level: int = 3, + messages: int = 420, + voicetime: int = 3600, + stars: int = 69, + prestige: int = 0, + prestige_emoji: t.Optional[t.Union[bytes, str]] = None, + balance: int = 0, + currency_name: str = "Credits", + previous_xp: int = 100, + current_xp: int = 125, + next_xp: int = 200, + position: int = 3, + role_icon: t.Optional[t.Union[bytes, str]] = None, + blur: bool = False, + base_color: t.Tuple[int, int, int] = (255, 255, 255), + user_color: t.Optional[t.Tuple[int, int, int]] = None, + stat_color: t.Optional[t.Tuple[int, int, int]] = None, + level_bar_color: t.Optional[t.Tuple[int, int, int]] = None, + font_path: t.Optional[t.Union[str, Path]] = None, + render_gif: bool = False, + debug: bool = False, + **kwargs, +) -> t.Tuple[bytes, bool]: + """Generate a modern, sleek profile card using Wand/ImageMagick""" + + # Set default colors for modern theme + user_color = user_color or (88, 101, 242) # Discord Blurple + stat_color = stat_color or (255, 255, 255) # White + level_bar_color = level_bar_color or (88, 101, 242) # Discord Blurple + + # Create base canvas (1050x320) + with Image(width=1050, height=320) as card: + # Set up background + if background_bytes: + with Image(blob=background_bytes) as bg: + bg.transform(resize='1050x320^') + bg.crop(width=1050, height=320, gravity='center') + if blur: + bg.gaussian_blur(sigma=15) + card.composite(bg, 0, 0) + else: + # Create gradient background + card.gradient('linear-gradient', 'navy-darkblue') + + # Add frosted glass effect overlay + with Image(width=1050, height=320, background=Color('rgba(255, 255, 255, 0.1)')) as overlay: + overlay.gaussian_blur(sigma=50) + card.composite(overlay, 0, 0) + + # Create circular avatar mask + with Drawing() as draw: + # Avatar circle (left side) + avatar_size = 200 + avatar_x = 60 + avatar_y = 60 + + if avatar_bytes: + with Image(blob=avatar_bytes) as avatar: + # Resize avatar + avatar.resize(avatar_size, avatar_size) + # Create circular mask + with Image(width=avatar_size, height=avatar_size, background=Color('transparent')) as mask: + draw.circle((avatar_size/2, avatar_size/2), (0, avatar_size/2)) + draw.draw(mask) + # Apply mask to avatar + avatar.composite_channel('alpha', mask, 'copy_alpha', 0, 0) + # Add avatar to card + card.composite(avatar, avatar_x, avatar_y) + + # Add modern stats panel + with Image(width=700, height=280, background=Color('rgba(0, 0, 0, 0.5)')) as stats_panel: + stats_panel.gaussian_blur(sigma=2) + stats_panel.border_color = Color('rgba(255, 255, 255, 0.1)') + stats_panel.border_width = 1 + card.composite(stats_panel, 310, 20) + + # Add text elements + draw.font = str(font_path) if font_path else '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf' + draw.font_size = 40 + draw.fill_color = Color('rgb({}, {}, {})'.format(*user_color)) + + # Level text + level_text = f"LEVEL {level}" + if prestige > 0: + level_text = f"P{prestige} • {level_text}" + draw.text(340, 70, level_text) + + # Stats + draw.font_size = 25 + draw.fill_color = Color('white') + y_pos = 120 + + # Left column + draw.text(340, y_pos, f"Messages: {humanize_number(messages)}") + draw.text(340, y_pos + 40, f"Voice Time: {imgtools.abbreviate_time(voicetime)}") + draw.text(340, y_pos + 80, f"Stars: {humanize_number(stars)}") + + # Right column + if balance is not None: + draw.text(600, y_pos, f"Balance: {humanize_number(balance)} {currency_name}") + draw.text(600, y_pos + 40, f"Rank: #{humanize_number(position)}") + + # Progress bar + progress = (current_xp - previous_xp) / (next_xp - previous_xp) + bar_width = 620 + bar_height = 30 + bar_x = 340 + bar_y = 240 + + # Background bar + draw.fill_color = Color('rgba(0, 0, 0, 0.3)') + draw.rectangle(bar_x, bar_y, bar_x + bar_width, bar_y + bar_height) + + # Progress bar + progress_width = int(bar_width * progress) + draw.fill_color = Color('rgb({}, {}, {})'.format(*level_bar_color)) + if progress_width > 0: + draw.rectangle(bar_x, bar_y, bar_x + progress_width, bar_y + bar_height) + + # XP Text + draw.font_size = 20 + draw.fill_color = Color('white') + xp_text = f"XP: {humanize_number(current_xp)} / {humanize_number(next_xp)}" + draw.text(bar_x + (bar_width - len(xp_text) * 10) / 2, bar_y - 15, xp_text) + + # Progress percentage + percent_text = f"{int(progress * 100)}%" + draw.text(bar_x + progress_width - 30, bar_y + 5, percent_text) + + # Apply all drawings + draw.draw(card) + + # Save the result + card.format = 'webp' + card.compression_quality = 95 + + buffer = BytesIO() + card.save(buffer) + return buffer.getvalue(), False + +if __name__ == "__main__": + # Test code + logging.basicConfig(level=logging.DEBUG) + test_avatar = (imgtools.ASSETS / "tests" / "default.png").read_bytes() + res, _ = generate_modern_profile( + avatar_bytes=test_avatar, + debug=True + ) + (imgtools.ASSETS / "tests" / "modern_result.webp").write_bytes(res) \ No newline at end of file diff --git a/levelup/shared/profile.py b/levelup/shared/profile.py index 21e9b07..8bbc129 100644 --- a/levelup/shared/profile.py +++ b/levelup/shared/profile.py @@ -15,7 +15,7 @@ from redbot.core.utils.chat_formatting import box, humanize_number from ..abc import MixinMeta from ..common import formatter, utils from ..common.models import Profile -from ..generator.styles import default, runescape +from ..generator.styles import default, runescape, modern log = logging.getLogger("red.vrt.levelup.shared.profile") _ = Translator("LevelUp", __file__) @@ -325,12 +325,15 @@ class ProfileFormatting(MixinMeta): except Exception as e: log.error("Failed to fetch profile from internal API", exc_info=e) - # By default we'll use the bundled generator + # By default we'll use the modern generator funcs = { - "default": default.generate_default_profile, + "modern": modern.generate_modern_profile, # New default modern style + "default": default.generate_default_profile, # Old style "runescape": runescape.generate_runescape_profile, } + profile_style = profile.style if profile.style in funcs else "modern" # Default to modern style + def _run() -> discord.File: img_bytes, animated = funcs[profile_style](**kwargs) ext = "gif" if animated else "webp" diff --git a/pyproject.toml b/pyproject.toml index 1518507..92fe79a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,3 +7,7 @@ line-length = 99 target-version = ['py38'] include = '\.pyi?$' + +[tool.poetry.dependencies] +python = ">=3.8.1,<4.0" +Wand = "^0.6.11" # ImageMagick Python bindings for modern image processing