# -*- coding: utf-8 -*- """ ============================================================== ماذا يوجد داخل كرة الغولف؟ — فيديو Shorts (9:16) ============================================================== يحاكي أسلوب الفيديو المرجعي (YouTube Shorts): • صيغة عمودية 1080x1920 @ 30fps • مشاهد سريعة مع تشويق في البداية (Hook) • نصوص عربية كبيرة على الشاشة • نهاية بمناداة المشاهدين للتعليقات (CTA) المتطلبات: pip install pillow arabic-reshaper python-bidi sudo apt-get install ffmpeg الخطوط (Tajawal من Google Fonts) في مجلد ./fonts التشغيل: python golf_ball_short.py # إخراج الفيديو النهائي python golf_ball_short.py --test # معاينة إطار واحد لكل مشهد فقط ============================================================== """ import os, sys, math, shutil, subprocess from PIL import Image, ImageDraw, ImageFont, ImageFilter, features import arabic_reshaper from bidi.algorithm import get_display # إذا كانت Pillow مبنية مع libraqm فهي تشكّل العربية وتعالج RTL تلقائياً، # ولا نستخدم arabic_reshaper/bidi إطلاقاً (وإلا يتشكّل النص مرتين فينفكك). HAS_RAQM = features.check("raqm") # ---------------------------------------------------------- إعدادات عامة W, H = 1080, 1920 FPS = 30 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) FONT_DIR = os.path.join(BASE_DIR, "fonts") FRAMES_DIR = os.path.join(BASE_DIR, "frames") TEST_MODE = "--test" in sys.argv F_TITLE = os.path.join(FONT_DIR, "Tajawal-Bold.ttf") F_BODY = os.path.join(FONT_DIR, "Tajawal-Regular.ttf") def font(size, bold=True): return ImageFont.truetype(F_TITLE if bold else F_BODY, size) # ---------------------------------------------------------- تهيئة النص العربي (تشكيل + اتجاه RTL) def ar(text): """إرجاع نص جاهز للرسم: نص خام مع raqm، أو معالَج بـ reshaper+bidi بدونه""" if HAS_RAQM: return text return get_display(arabic_reshaper.reshape(text)) def text_center(d, y, txt, f, fill, stroke=0, stroke_fill=(0,0,0)): kw = {"direction": "rtl"} if HAS_RAQM else {} d.text((W//2, y), ar(txt), font=f, fill=fill, anchor="mm", stroke_width=stroke, stroke_fill=stroke_fill, **kw) def ease(t): # تخفيف حركة سلس t = max(0.0, min(1.0, t)) return 3*t*t - 2*t*t*t # ---------------------------------------------------------- ألوان وخطوط المشهد GREEN_DARK = (6, 45, 30) GREEN_LIGHT = (16, 92, 60) WHITE = (255, 255, 255) GOLD = (255, 200, 60) ORANGE = (255, 140, 40) CORE_RED = (150, 30, 30) CORE_DARK = (90, 15, 15) SHADOW = (0, 0, 0, 120) def bg_gradient(img, top=GREEN_DARK, bottom=GREEN_LIGHT): for y in range(H): k = y / H r = int(top[0]*(1-k) + bottom[0]*k) g = int(top[1]*(1-k) + bottom[1]*k) b = int(top[2]*(1-k) + bottom[2]*k) ImageDraw.Draw(img).line([(0, y), (W, y)], fill=(r, g, b)) def draw_dimples(d, cx, cy, r, tint=(225,225,225), inner=0): """رسم الحفّات داخل دائرة نصف قطرها r، مع إمكانية استثنائها داخل inner""" rows = int(r // 34) for i in range(-rows, rows + 1): for j in range(-rows, rows + 1): dx = i * 60 + (30 if i % 2 else 0) dy = j * 52 dist2 = dx*dx + dy*dy if dist2 < (r - 14) ** 2 and dist2 > inner ** 2: pr = 13 d.ellipse([cx+dx-pr, cy+dy-pr, cx+dx+pr, cy+dy+pr], fill=tint, outline=(200, 200, 200), width=2) def draw_golf_ball(d, cx, cy, r, t=1.0): """كرة الغولف كاملة مع ظل وحفّات — t للتحكم بالظهور (0..1)""" r = int(r * t) if r <= 1: return d.ellipse([cx - r*0.9, cy + r*0.95, cx + r*0.9, cy + r*1.25], fill=SHADOW) d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=WHITE, outline=(190,190,190), width=4) draw_dimples(d, cx, cy, r) d.ellipse([cx - r*0.55, cy - r*0.75, cx - r*0.1, cy - r*0.35], fill=(250, 250, 250)) def draw_cutaway(d, cx, cy, R, highlight=None): """ مقطع عرضي (Cutaway) يكشف الطبقات من الخارج للداخل: الغلاف الأبيض ثم المانتل البرتقالي ثم النواة الحمراء. highlight = 'cover' | 'mantle' | 'core' | None → إطار متوهج حول الطبقة المُبرزة. """ cover_r = R mantle_r = int(R * 0.72) core_r = int(R * 0.42) d.ellipse([cx-cover_r, cy-cover_r, cx+cover_r, cy+cover_r], fill=WHITE, outline=(160,160,160), width=5) draw_dimples(d, cx, cy, cover_r, tint=(230,230,230), inner=mantle_r) d.ellipse([cx-mantle_r, cy-mantle_r, cx+mantle_r, cy+mantle_r], fill=ORANGE, outline=(200,110,20), width=4) d.ellipse([cx-core_r, cy-core_r, cx+core_r, cy+core_r], fill=CORE_RED, outline=CORE_DARK, width=6) ring = {"cover": cover_r, "mantle": mantle_r, "core": core_r}.get(highlight) if ring: glow = GOLD if highlight != "core" else (255, 90, 90) w = 14 + int(4 * math.sin(2 * math.pi * (ring % 7))) d.ellipse([cx-ring, cy-ring, cx+ring, cy+ring], outline=glow, width=w) d.ellipse([cx-cover_r*0.62, cy-cover_r*0.85, cx-cover_r*0.18, cy-cover_r*0.45], fill=(255,255,255,60)) def label(d, y, txt, color, target, side="right"): """تسمية بمؤشر يشير إلى إحداثيات الطبقة المستهدفة داخل الكرة""" kw = {"direction": "rtl"} if HAS_RAQM else {} f = font(56) tw = d.textlength(ar(txt), font=f) if side == "right": x0, x1 = int(W*0.56), int(W*0.93) start = (x1 - tw/2 - 10, y) else: x0, x1 = int(W*0.07), int(W*0.44) start = (x0 + tw/2 + 10, y) d.line([start, target], fill=color, width=6) d.ellipse([target[0]-11, target[1]-11, target[0]+11, target[1]+11], fill=color) d.rounded_rectangle([x0-20, y-52, x1+20, y+52], radius=28, fill=(0,0,0,170), outline=color, width=4) d.text(((x0+x1)//2, y), ar(txt), font=f, fill=color, anchor="mm", **kw) def progress_bar(d, k): """شريط تقدّم سفلي مثل صناعة الشورتس""" w_bar = int(W * 0.86 * k) d.rounded_rectangle([W*0.07, H-70, W*0.93, H-46], radius=12, fill=(255,255,255,60)) if w_bar > 20: d.rounded_rectangle([W*0.07, H-70, W*0.07+w_bar, H-46], radius=12, fill=GOLD) # ---------------------------------------------------------- المشاهد SCENES = [] # (المدة بالثواني, دالة الرسم draw(d, t)) def scene(sec): def deco(fn): SCENES.append((sec, fn)) return fn return deco @scene(3.0) # ---------- المشهد 1: الخطاف (Hook) def scene_hook(d, t): k = ease(t) cy = -300 + int((H*0.40 + 300) * min(1, k*1.15)) if k > 0.85: cy += int(40 * math.sin((k-0.85)/0.15 * math.pi) * -1) draw_golf_ball(d, W//2, cy, 210, t=min(1, k*2)) if t > 0.30: text_center(d, H*0.72, "ماذا يوجد داخل كرة الغولف؟", font(86), WHITE, stroke=6) text_center(d, H*0.80, "الأمر أغرب مما تتخيل..", font(56), GOLD, stroke=4) @scene(4.5) # ---------- المشهد 2: الغلاف (Cover) def scene_cover(d, t): R = int(280 + 190 * ease(t)) # تقريب (Zoom) تدريجي cy = int(H*0.38) draw_cutaway(d, W//2, cy, R, 'cover' if t > 0.25 else None) if t > 0.25: label(d, int(H*0.75), "الغلاف الخارجي", GOLD, (W//2 + int(R*0.5), cy + int(R*0.5)), "right") text_center(d, int(H*0.86), "يوريثان ناعم أو أيونومر (سيرلين) صلب", font(50), WHITE, stroke=4) text_center(d, int(H*0.91), "يوفر التحكم والدوران قرب المنصة", font(42), (220,220,220), stroke=3) @scene(4.5) # ---------- المشهد 3: المانتل (Mantle) def scene_mantle(d, t): R = int(400 + 140 * ease(t)) cy = int(H*0.38) draw_cutaway(d, W//2, cy, R, 'mantle' if t > 0.25 else None) if t > 0.25: mr = int(R * 0.72) label(d, int(H*0.75), "طبقة المانتل الوسطى", ORANGE, (W//2 - int(mr*0.55), cy - int(mr*0.55)), "right") text_center(d, int(H*0.86), "راتنج أيونومر يرفع قوة الضرب", font(50), WHITE, stroke=4) text_center(d, int(H*0.91), "ينقل الطاقة من النواة إلى الغلاف", font(42), (220,220,220), stroke=3) @scene(4.5) # ---------- المشهد 4: النواة (Core) def scene_core(d, t): R = int(460 + 160 * ease(t)) cy = int(H*0.38) draw_cutaway(d, W//2, cy, R, 'core' if t > 0.25 else None) if t > 0.25: cr = int(R * 0.42) label(d, int(H*0.75), "النواة المطاطية", (255, 90, 90), (W//2 - int(cr*0.5), cy + int(cr*0.3)), "right") text_center(d, int(H*0.86), "بولي بيوتادايين عالي الطاقة", font(50), WHITE, stroke=4) text_center(d, int(H*0.91), "مخزن القوة الحقيقي للضربة!", font(42), GOLD, stroke=3) @scene(4.0) # ---------- المشهد 5: معلومة الحفّات def scene_dimples(d, t): draw_golf_ball(d, W//2, int(H*0.42), int(280 + 40*math.sin(t*6))) text_center(d, int(H*0.72), "هل تعلم؟", font(64), GOLD, stroke=5) text_center(d, int(H*0.79), "عدد الحفّات من 300 إلى 500", font(62), WHITE, stroke=5) text_center(d, int(H*0.85), "غالباً 336 حفّة بالضبط!", font(54), (230,230,230), stroke=4) text_center(d, int(H*0.90), "تقلل مقاومة الهواء وترفع الكرة", font(44), (210,210,210), stroke=3) @scene(4.0) # ---------- المشهد 6: الخاتمة + نداء التعليقات def scene_cta(d, t): draw_golf_ball(d, W//2, int(H*0.40), 240) text_center(d, int(H*0.70), "كرة صغيرة.. 3 طبقات هندسية!", font(60), WHITE, stroke=5) text_center(d, int(H*0.78), "منشن صديقه اللي يلعب غولف", font(54), GOLD, stroke=4) text_center(d, int(H*0.85), "واكتب بالتعليقات: أي طبقة فاجأتك؟", font(50), (230,230,230), stroke=4) if t > 0.5: text_center(d, int(H*0.92), "تابعنا للمزيد", font(46, bold=False), (180,255,180), stroke=3) # ---------------------------------------------------------- محرك الإخراج TOTAL = sum(s for s, _ in SCENES) def render_frame(idx_global, scene_idx, t): img = Image.new("RGB", (W, H)) bg_gradient(img) d = ImageDraw.Draw(img, "RGBA") SCENES[scene_idx][1](d, t) progress_bar(d, (idx_global + t) / TOTAL) return img def main(): if os.path.exists(FRAMES_DIR): shutil.rmtree(FRAMES_DIR) os.makedirs(FRAMES_DIR) if TEST_MODE: # معاينة: إطار واحد من منتصف كل مشهد للتحقق من العربية والطبقات i = 0 for si, (sec, _) in enumerate(SCENES): render_frame(i, si, 0.65).save(os.path.join(FRAMES_DIR, f"preview_{si}.png")) i += sec print(f"TEST OK: {len(SCENES)} preview frames -> {FRAMES_DIR}") return n = 0 for si, (sec, _) in enumerate(SCENES): for f in range(int(sec * FPS)): t = f / (sec * FPS) render_frame(n / FPS, si, t).save(os.path.join(FRAMES_DIR, f"f_{n:05d}.png")) n += 1 print(f"scene {si+1}/{len(SCENES)} done") out = os.path.join(BASE_DIR, "golf_ball_inside.mp4") subprocess.run([ "ffmpeg", "-y", "-framerate", str(FPS), "-i", os.path.join(FRAMES_DIR, "f_%05d.png"), "-c:v", "libx264", "-pix_fmt", "yuv420p", "-vf", "scale=1080:1920", out ], check=True) print(f"DONE -> {out} ({TOTAL:.0f}s, {n} frames)") if __name__ == "__main__": main()
Hosted & Compiled with HtmlRunner.com