from PIL import Image, ImageDraw, ImageFont config = { "ids": ["falling_words"], "name": "Falling Words", "panels": ["color", "font", "size", "position", "radius", "padding"], "default_font": "vazir" } # قالب نمایش زنده در مرورگر (با انیمیشن CSS) frontend_template = """ {{WORD}} """ 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' # دریافت رنگ متن فقط از تنظیمات اختصاصی (پیش‌فرض سفید مطلق) - قطع ارتباط با استایل‌های دیگر text_hex = style_config.styleColors.get('falling_words', '#FFFFFF') base_text_color = color_parser(text_hex, (255, 255, 255, 255)) # خواندن رنگ کادر از استایل (پیش‌فرض نارنجی F77D57) custom_bg_hex = style_config.styleBgColors.get('falling_words', '#F77D57') box_color = color_parser(custom_bg_hex, (247, 125, 87, 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 # محاسبه عرض کادر max_line_width = 0 for m in line_metrics: if m["width"] > max_line_width: max_line_width = m["width"] box_center_x = width / 2 + (style_config.x or 0) box_width = max_line_width + (style_config.paddingX * 2) # ۱. محاسبه نسبت ابعاد ویدیو ratio = height / width # ۲. تنظیم موقعیت عمودی (اگر عدد مثبت باشد پایین می‌رود و اگر منفی باشد بالا) if ratio > 1.6: # مخصوص ویدیوهای عمودی (طبق تست شما: ۱۲) box_center_y_adjustment = 12 elif ratio > 1.1: # مخصوص ویدیوهای مربعی box_center_y_adjustment = 8 else: # مخصوص ویدیوهای افقی box_center_y_adjustment = 5 # ۳. اصلاحیه بصری سقف کادر (تراز عمودی متن) visual_top_correction = int(style_config.fontSize * 0.12) # ۳. لبه‌های چپ و راست box_x1 = box_center_x - (box_width / 2) box_x2 = box_center_x + (box_width / 2) # ۴. مختصات سقف و کف (فرمول دقیق کلاسیک) box_y1 = start_y_of_block - style_config.paddingY + box_center_y_adjustment + visual_top_correction box_y2 = start_y_of_block + total_block_height + style_config.paddingY + box_center_y_adjustment - (line_height_px * 0.3) # --- رفع باگ: جلوگیری از منفی شدن ارتفاع کادر در پدینگ‌های کم --- if box_y2 <= box_y1: box_y2 = box_y1 + 1 # حداقل ۱ پیکسل ارتفاع داشته باشد تا ارور ندهد # رسم کادر (شرط len(lines) اضافه شد تا در سکوت اصلا کادر نکشد) if len(lines) > 0 and style_config.backType in ['solid', 'transparent']: if style_config.backType == 'transparent' and box_color[3] == 255: box_color = (box_color[0], box_color[1], box_color[2], 160) draw.rounded_rectangle([box_x1, box_y1, box_x2, box_y2], radius=style_config.radius, fill=box_color) # --- رسم کلمات با منطق الگوبرداری شده از آبی نئون --- text_start_y = box_y1 + (style_config.paddingY * 0.35) 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 current_render_time = getattr(style_config, 'current_render_time', 0.0) for line_idx, enumerate_metrics in enumerate(line_metrics): metrics = enumerate_metrics start_x = (width + metrics["width"]) / 2 + (style_config.x or 0) if is_rtl else (width - metrics["width"]) / 2 + (style_config.x or 0) cursor_x = start_x 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 y_offset = 0 alpha_factor = 1.0 # منطق اصلی نمایش کلمه 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 = current_render_time - w_info.start anim_dur = 0.3 # زمان سقوط (ثانیه) if time_passed < anim_dur: progress = time_passed / anim_dur # انیمیشن نرم Ease-Out ease = 1 - (1 - progress) * (1 - progress) y_offset = -40 * (1 - ease) alpha_factor = ease else: y_offset = 0 alpha_factor = 1.0 else: # کلماتی که هنوز زمانشان نرسیده نباید رسم شوند should_draw = False else: # بک‌آپ برای حالتی که اطلاعات کلمه نیست (در ادیتور) if active_idx != -1: should_draw = (global_word_counter <= active_idx) else: should_draw = False # اصلاح مشکل نمایش یکباره: در گپ‌ها چیزی نشان نده if should_draw: # تعیین رنگ پایه this_color = base_text_color # اگر کلمه فعال است (زمان رندر بین شروع و پایان کلمه است) if getattr(style_config, 'useActiveColor', True) and w_info and current_render_time >= w_info.start and current_render_time < w_info.end: active_hex = style_config.styleActiveColors.get('falling_words', '#FFEB3B') this_color = color_parser(active_hex, (255, 235, 59, 255)) 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.text((word_x, text_y_pos + y_offset), 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