Spaces:
Sleeping
Sleeping
| 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 = """ | |
| <span style=" | |
| display: inline-block; | |
| vertical-align: middle; | |
| margin: 0 4px; | |
| animation: dropIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; | |
| opacity: 0; | |
| transform: translateY(-20px); | |
| font-family: inherit; | |
| "> | |
| {{WORD}} | |
| </span> | |
| <style> | |
| @keyframes dropIn { | |
| 0% { opacity: 0; transform: translateY(-30px); } | |
| 100% { opacity: 1; transform: translateY(0); } | |
| } | |
| </style> | |
| """ | |
| 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 |