# -*- 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()