From 653b16cb5e5429b614171784826fb84eeefe5390 Mon Sep 17 00:00:00 2001 From: Valerie Date: Mon, 26 May 2025 20:58:12 -0400 Subject: [PATCH] Update pyproject.toml to include Python and Wand dependencies. Modify default.py to enhance profile layout with adjusted positions and improved glass morphism effect. Update profile.py to set modern profile generation as the default style, ensuring a more contemporary user experience. --- levelup/generator/styles/default.py | 14 +-- levelup/generator/styles/modern.py | 170 ++++++++++++++++++++++++++++ levelup/shared/profile.py | 9 +- pyproject.toml | 4 + 4 files changed, 187 insertions(+), 10 deletions(-) create mode 100644 levelup/generator/styles/modern.py 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