from PIL import Image, ImageDraw, ImageFont, ImageFilter import math # --- استایل «طراحی هوشمند» (AI Dynamic) --- # هوش مصنوعی (Gemini) هرگز کد اجرا نمی‌کند؛ فقط یک شیء طراحی محدود (ai_design) با # چند فیلد از پیش تعریف‌شده برمی‌گرداند (entry, exit, timing, stagger, glow, رنگ‌ها). # این استایل دیگر هیچ کادر/باکس/پس‌زمینه‌ای دور متن یا دور کلمات نمی‌سازد؛ تمرکز فقط # روی حرکت ورود/خروج کلمات و رنگ متن است (و در صورت درخواست، درخشش نور اطراف متن). # مقادیر آن قبلاً در app.py (_clamp_ai_design) کاملاً اعتبارسنجی و clamp شده‌اند. # این فایل صرفاً همان پارامترها را می‌خواند و واقعاً فریم را رسم می‌کند؛ به همین دلیل # خروجی همیشه امن، پایدار و بدون امکان کرش است. config = { "ids": ["ai_dynamic"], "name": "طراحی هوشمند (AI)", "panels": ["font", "size", "position"], "default_font": "vazir" } frontend_template = """ {{WORD}} """ DEFAULT_DESIGN = { "entry": "fall", "exit": "fade", "timing": "smooth", "stagger": 0.04, "glow": "none", "primaryColor": "#FFFFFF", "activeColor": "#FFD700", "glowColor": "#7C4DFF", "font": "vazir", "fontSize": 60, } def _get_design(style_config): raw = getattr(style_config, 'ai_design', None) or {} if not isinstance(raw, dict): raw = {} d = dict(DEFAULT_DESIGN) for k in DEFAULT_DESIGN: if k in raw and raw[k] is not None: d[k] = raw[k] # سازگاری با طراحی‌های قدیمی‌تر که ممکن است هنوز shapeColor داشته باشند if "glowColor" not in raw and "shapeColor" in raw: d["glowColor"] = raw["shapeColor"] return d def _ease(name, p): p = max(0.0, min(1.0, p)) if name == "linear": return p if name == "punch": # شروع تند و توقف نرم با کمی سرریز (overshoot) خفیف return 1 - pow(1 - p, 3) if name == "elastic": if p <= 0 or p >= 1: return p return pow(2, -10 * p) * math.sin((p * 10 - 0.75) * (2 * math.pi) / 3) + 1 # smooth (پیش‌فرض) return 1 - (1 - p) * (1 - p) def draw_frame(draw, img, width, height, style_config, lines, line_metrics, active_idx, font, color_parser, word_infos=None): is_rtl = getattr(style_config, 'text_direction', 'rtl') != 'ltr' design = _get_design(style_config) primary_color = color_parser(design["primaryColor"], (255, 255, 255, 255)) active_color = color_parser(design["activeColor"], (255, 215, 0, 255)) glow_color = color_parser(design["glowColor"], (124, 77, 255, 255)) line_height_px = int(style_config.fontSize * 1.5) total_block_height = len(lines) * line_height_px bottom_reference = height - style_config.marginV start_y_of_block = bottom_reference - total_block_height current_render_time = getattr(style_config, 'current_render_time', 0.0) text_start_y = start_y_of_block vertical_corr = int(style_config.fontSize * 0.1) current_line_y = text_start_y - vertical_corr space_w = draw.textlength(" ", font=font) global_word_counter = 0 entry = design["entry"] exit_type = design["exit"] timing = design["timing"] stagger = max(0.0, min(0.12, float(design["stagger"]) if isinstance(design["stagger"], (int, float)) else 0.04)) glow = design["glow"] ANIM_DUR = 0.3 EXIT_DUR = 0.3 glow_blur_radius = {"none": 0, "soft": 4, "strong": 8}.get(glow, 0) glow_intensity = {"none": 0, "soft": 160, "strong": 230}.get(glow, 0) for line_idx, metrics in enumerate(line_metrics): cursor_x = (width + metrics["width"]) / 2 + (style_config.x or 0) if is_rtl else (width - metrics["width"]) / 2 + (style_config.x or 0) text_y_pos = current_line_y for w_i, word in enumerate(metrics["words"]): w_len = metrics["word_widths"][w_i] word_x = (cursor_x - w_len) if is_rtl else cursor_x should_draw = False dx, dy, alpha_factor = 0.0, 0.0, 1.0 is_active_word = False w_info = None extra_delay = stagger * w_i # تاخیر آبشاری بین کلمات پشت‌سرهم داخل یک خط if word_infos and global_word_counter < len(word_infos): w_info = word_infos[global_word_counter] if current_render_time >= w_info.start: should_draw = True time_passed = max(0.0, (current_render_time - w_info.start) - extra_delay) progress = min(1.0, time_passed / ANIM_DUR) eased = _ease(timing, progress) if entry == "fall": dy = -70 * (1 - eased) elif entry == "rise": dy = 65 * (1 - eased) elif entry == "pop": dy = -18 * (1 - eased) elif entry == "slide_left": dx = 85 * (1 - eased) elif entry == "slide_right": dx = -85 * (1 - eased) # "fade" فقط آلفا دارد alpha_factor = max(0.0, min(1.0, eased)) is_active_word = current_render_time >= w_info.start and current_render_time < w_info.end # --- افکت خروج بعد از پایان گفتار این کلمه --- if current_render_time >= w_info.end and exit_type != "none": time_after = current_render_time - w_info.end exit_progress = min(1.0, time_after / EXIT_DUR) exit_eased = _ease("smooth", exit_progress) if exit_type == "fade": alpha_factor = max(0.4, 1.0 - exit_eased * 0.6) elif exit_type == "shrink": alpha_factor = max(0.35, 1.0 - exit_eased * 0.65) dy += 6 * exit_eased elif exit_type == "fall": dy += 25 * exit_eased alpha_factor = max(0.0, 1.0 - exit_eased) elif exit_type == "rise": dy -= 25 * exit_eased alpha_factor = max(0.0, 1.0 - exit_eased) else: should_draw = False else: if active_idx != -1: should_draw = (global_word_counter <= active_idx) is_active_word = (global_word_counter == active_idx) else: should_draw = False if should_draw and alpha_factor > 0.01: this_color = primary_color if getattr(style_config, 'useActiveColor', True) and is_active_word: this_color = active_color if word_infos and global_word_counter < len(word_infos): w_obj = word_infos[global_word_counter] if hasattr(w_obj, 'color') and w_obj.color: this_color = color_parser(w_obj.color, this_color) final_alpha = int(this_color[3] * alpha_factor) final_color = (this_color[0], this_color[1], this_color[2], final_alpha) draw_x = word_x + dx draw_y = text_y_pos + dy # --- درخشش نور نرم اطراف متن (بلور واقعی)؛ دیگر هیچ کادر/باکسی رسم نمی‌شود --- if glow_blur_radius > 0: pad = glow_blur_radius * 3 + 8 layer_w = int(w_len) + pad * 2 layer_h = int(style_config.fontSize * 1.3) + pad * 2 if layer_w > 0 and layer_h > 0: glow_layer = Image.new('RGBA', (layer_w, layer_h), (0, 0, 0, 0)) glow_draw = ImageDraw.Draw(glow_layer) glow_a = int(glow_intensity * alpha_factor) glow_draw.text((pad, pad), word, font=font, fill=(*glow_color[:3], glow_a), direction=getattr(style_config, 'text_direction', 'rtl'), language=getattr(style_config, 'lang_code', 'fa')) blurred = glow_layer.filter(ImageFilter.GaussianBlur(glow_blur_radius)) img.paste(blurred, (int(draw_x - pad), int(draw_y - pad)), blurred) draw.text((draw_x, draw_y), word, font=font, fill=final_color, direction=getattr(style_config, 'text_direction', 'rtl'), language=getattr(style_config, 'lang_code', 'fa')) cursor_x += (-(w_len + space_w) if is_rtl else (w_len + space_w)) global_word_counter += 1 current_line_y += line_height_px