محرك الحركة
تبدو الحركة سحرًا لأن كل إطار بسيط; السحر هو الرياضيات الكواليسية التي تصل إطارًا بإطار. يبني هذا المشروع تلك الكواليس في Python نقي: تخفيف smoothstep بين رقمين, وكائنات تحمل سرعة وترتد عن جدران لوحة 30×10, ومحرك بخطوة زمنية ثابتة يخطو المشهد كله لكل إطار, ومسارات بإطارات رئيسية باستقراء ميسّر, وإطارات مُصدَّرة كملفات نصية يمكنك إعادتها. يعمل المحرك حتميًا ، نفس النقاط تهبط في نفس الخلايا كل مرة ، فتستطيع التحقق من كل ادعاء في هذا الدليل قبل أن تجعل النقاط ترقص. إنه محرك نصّي أولًا: «الفيديو» كومة إطارات .txt يمكنك لصقها في أي مكان.
هذا يفترض صفوفًا وطرائق زائد حسابًا أساسيًا بعوامات. مشروع اختياري وغير مُقيَّم ، راجع مشاريع من العالم الحقيقي للاطلاع على القائمة الكاملة والنامية.
🎯 ما ستفعله
- كتابة مساعدات الرياضيات:
clampوlerpومنحنى تخفيف smoothstep. - تعريف
Spriteيتحرك بسرعة ويرتد عن حواف اللوحة. - بناء
Sceneيقدّم الكائنات على شبكة نصية, وEngineيخطو ويطبع الإطارات. - إضافة مسارات مفاتيح الإطارات كي ينعم كائن على طول مسار بدل الانجراف.
- تصدير الإطارات إلى ملفات وإعادة تجميعها كشريط أفلام.
أين تُشغّل هذا
محليًا باستخدام uv هو المسار الموصى به ، المحرك Python نقي (يلزم pathlib فقط), فـuv init يمنحك كل شيء.
Google Colab وKaggle Notebooks وBinder تشغّل كل خطوة دون تعديل ، اللوحة والتخفيف رياضيات وسلاسل فقط, لا استدعاءات خاصة بالمنصة, ودفتر خلية-خلية يلائم التصميم إطار-بإطار جيدًا.
الإعداد
كل ما يلزم قبل وجود الإطار الأول.
أعِدَّ المشروع
uv init animation-engine
cd animation-engine
لا تبعيات. اللوحة شبكة سلاسل; والتصدير يكتب ملفات نصية عادية.
✅ قائمة التحقق
- ✅ يُنشئ
uv init animation-engineالمشروع وملفmain.py. - ✅ ينجح
uv run python3 -c "from pathlib import Path"(pathlib هو الاستيراد الوحيد).
🤔 سؤال (أسئلة) سقراطي(ة) :
- لوحة نقاط بشخصية متحركة مملة ، لكن كل محرك تقديم, من هذا إلى أفلام السينما, ما هو إلا «شبكة, تُحدَّث بمعدل ثابت». ما الذي يجعل الرياضيات بين التحديثات, لا الشبكة, هي المحرك الفعلي؟
- يعمل المشروع في دفتر, ومع ذلك تصدّر إطاراتك كملفات نصية. ماذا يشتري لك فيلم من 10 صفوف نقاط لا يستطيع حلقة عرض حية إعطاءه ، وماذا تخسر لو اتجهت الاتجاه الآخر؟
الخطوة 1: الرياضيات وراء الحركة
تختزل كل حركة إلى أسئلة عددية مجهرية: «انتقل من 0 إلى 10, لكن أين أنا في منتصف الطريق؟» تكتب الخطوة 1 الإجابات الثلاث التي ستعيد استخدامها في كل مكان.
1.1 clamp وlerp وsmoothstep
👟 تلميح البداية : اكتب clamp(v, lo, hi) وlerp(a, b, t) وsmoothstep(t) ، الأخيرة هي منحنى الدخول-الخروج الشهير t²·(3 − 2t).
# main.py
def clamp(v, lo, hi):
return max(lo, min(hi, v))
def lerp(a, b, t):
return a + (b - a) * t
def smoothstep(t):
t = clamp(t, 0.0, 1.0)
return t * t * (3 - 2 * t)
print("clamp(13, 0, 10) =", clamp(13, 0, 10))
print("lerp(0, 10, 0.5) =", lerp(0, 10, 0.5))
print("smoothstep(0, .25, .5, .75, 1):",
smoothstep(0), smoothstep(0.25), smoothstep(0.5), smoothstep(0.75), smoothstep(1))
lerp(a, b, t) هو حصان العمل: عند t=0 أنت عند a, وعند t=1 عند b, وخطيًا بينهما. smoothstep هي شخصية التخفيف: ما زالت تطبّق 0→0 و1→1, لكنها تقضي منتصف الحركة سريعًا والبداية والنهاية ببطء ، يُعيد smoothstep(0.5) القيمة 0.5 بالضبط, لكن smoothstep(0.25) ليست إلا 0.15625, فيتمهل ثم يلحق. ذلك التباين هو ما يجعل الحركة الميسَّرة حية لا آلية.
🎯 الناتج المتوقع :
clamp(13, 0, 10) = 10
lerp(0, 10, 0.5) = 5.0
smoothstep(0, .25, .5, .75, 1): 0.0 0.15625 0.5 0.84375 1.0
🩹 إذا لم يعمل : إن لم يكن smoothstep(0.5) مساويًا 0.5, تحقق من الأس ، t*t*(3-2*t) لا t*t*t. إن طُبعت القيم 0 بلا كسور عشرية, فالوسائط كانت int وتسلّلت قسمة صحيحة إلى مكانٍ ما ، غذِّ عوامات.
1.2 ميسّر مسارًا كاملًا
👟 تلميح البداية : اربط smoothstep في lerp كي تتبع حركة المنحنى لا خطًا مستقيمًا.
# main.py (continued)
def eased_lerp(a, b, t):
return lerp(a, b, smoothstep(t))
print("eased_lerp(0, 10, .5) =", eased_lerp(0, 10, 0.5))
print("eased_lerp(0, 10, .25) =", eased_lerp(0, 10, 0.25))
يطابق eased_lerp عيّنة smoothstep أعلاه: عند t=0.25 لم تعبر إلا 1.5625 من رحلة الـ10 وحدات, لا 2.5. تبدأ النقطة ببطء, تتسارع خلال المنتصف, وتبطئ عند النهاية.
🎯 الناتج المتوقع :
eased_lerp(0, 10, .5) = 5.0
eased_lerp(0, 10, .25) = 1.5625
🩹 إذا لم يعمل : إن طبع eased_lerp(0, 10, .25) قيمة 2.5, فاستدعيت lerp(a, b, t) مباشرة, متخطيًا التخفيف.
1.3 تحقّق من الرياضيات
✅ قائمة التحقق
- ✅
clamp(13, 0, 10) == 10وclamp(-4, 0, 10) == 0. - ✅ يطبّق
smoothstep0→0 و1→1 و0.5→0.5 وهو متماثل حول المنتصف. - ✅ يتفق
eased_lerpمع أرقامsmoothstepالنقية.
🤔 سؤال (أسئلة) سقراطي(ة) :
smoothstepمتماثل:smoothstep(0.25) == 1 - smoothstep(0.75)(هنا0.84375). ما الحركة الحقيقية التي تحس كذاك ، تسارع, طفو, ثم تبطؤ ، وأي منحنى ستختار بدلًا منه لـرمية, حيث البداية سريعة والهبوط تحطّم؟clamp(t, 0, 1)داخلsmoothstepيصلح الإدخال خارج المدى بصمت. لماذا الصلاح الصامت جيد لتخفيف نقطة, وخطير إن أخفى نفس القصّ خطأً في, لنقل, حركة مؤشر قرص حرج للسلامة؟
الخطوة 2: الكائنات ، أشياء تتحرك
تحرّك الرياضيات أرقامًا; تحرّك الكائنات أشياء. تعطي الخطوة 2 كل شيء موضعًا وسرعة وحرفًا, وتقول «خطّني للأمام بمقدار dt ثانية».
2.1 صف Sprite
👟 تلميح البداية : اكتب Sprite(ch, x, y, vx=0.0, vy=0.0) مع update(dt) يدمج الموضع: x += vx · dt.
# main.py (continued)
class Sprite:
def __init__(self, ch, x, y, vx=0.0, vy=0.0):
self.ch = ch
self.x, self.y = float(x), float(y)
self.vx, self.vy = float(vx), float(vy)
def update(self, dt):
self.x += self.vx * dt
self.y += self.vy * dt
s = Sprite("o", 0.0, 5.0, vx=4.0)
for _ in range(5):
s.update(0.125)
print(round(s.x, 3), round(s.y, 3))
x += vx * dt هو تكامل Euler: يتقدم الموضع بالسرعة مضروبة في الزمن المنقضي. dt صغير = حركة ناعمة; dt هي الخطوة الزمنية الثابتة التي ستعتمدها في الخطوة 3. يبقى الموضع عائمًا هنا ويُقصّ إلى خلايا الشبكة عند العرض فقط ، ذلك العائم هو الحقيقة «بين الإطارات» التي لا تستطيع الشبكة حملها.
🎯 الناتج المتوقع : 2.5 5.0 ، خمس خطوات من 0.125s عند 4 وحدات/ث تقطع 5 × 0.5 = 2.5 وحدة, بالضبط.
🩹 إذا لم يعمل : إن كان الناتج 0.0 5.0, فـupdate لم يجرِ قط (إزاحة الحلقة خاطئة) أو لم يُضبط vx أبدًا. إن كان 40.0, فكان dt يساوي 1.0 ، مررت عدّ الإطارات كزمن.
2.2 ارتدادات الجدران
👟 تلميح البداية : أضف حدود لوحة ثابتة (W=30, H=10) إلى Sprite; في update, قصّ الموضع واعكس السرعة عند التلامس.
# main.py (continued)
class Sprite:
W, H = 30, 10
def __init__(self, ch, x, y, vx=0.0, vy=0.0):
self.ch = ch
self.x, self.y = float(x), float(y)
self.vx, self.vy = float(vx), float(vy)
def update(self, dt):
self.x += self.vx * dt
self.y += self.vy * dt
self.x = clamp(self.x, 0.0, self.W - 1)
self.y = clamp(self.y, 0.0, self.H - 1)
if self.x == 0.0 or self.x == self.W - 1:
self.vx = -self.vx
if self.y == 0.0 or self.y == self.H - 1:
self.vy = -self.vy
b = Sprite("*", 15.0, 2.0, vy=2.0)
for step in range(8):
b.update(0.125)
if step in (4, 7):
print("step", step + 1, "y =", round(b.y, 3), "vy =", b.vy)
يبقي القصّ الكائن على اللوحة; واختبار المساواة مع 0.0 أو W-1 يقلب السرعة مرة واحدة بالضبط لكل تلامس. يهبط * من y=2.0, ولأنه يقطع 0.25 وحدة لكل إطار, يبلغ الأرضية (y=9) نظيفًا وينقلب صاعدًا.
🎯 الناتج المتوقع :
step 5 y = 3.25 vy = 2.0
step 8 y = 4.0 vy = 2.0
(يهبط الارتداد لاحقًا في الجولة ، عارض الخطوة 3 يظهره.)
🩹 إذا لم يعمل : إن توقفت y عند 9.0 إلى الأبد, فـvy ينقلب لكنه ينقلب مجددًا في الإطار التالي ، يطلق فحص المساواة كل إطار بينما يستند ضد الجدار. يجب أن يغادر الموضع الجدار قبل أن يعاد تسليح الفحص (يحدث ذلك هنا, لأن السرعة تنعكس).
2.3 تحقّق من الكائن
✅ قائمة التحقق
- ✅
Sprite("o", 0, 5, vx=4)يتقدم2.5بعد خمس خطوات0.125. - ✅ كائن بسرعة
vxسالبة يتحرك يسارًا ويتحد عندx=0. - ✅ عند تلامس الجدار تنقلب السرعة مرة واحدة بالضبط, ويعود الكائن للداخل.
🤔 سؤال (أسئلة) سقراطي(ة) :
- لا يصطدم الكائن إلا بالجدران, لا بكائنات أخرى. ما الاختبار الإضافي الذي يحتاجه اصطدام كائنين ولا يحتاجه اصطدام الجدار ، وأيًّا من
x == 0مقابلabs(x - wall) < epsستريده له؟ - الموضع عائم; العرض يقصّ إلى الخلايا. إذا كانت السرعة
1وdtيساوي0.125, تبدو النقطة وكأنها «تقفز» كل 8 إطارات. هل ذلك ناعم أم مسنن عند 8 إطارات/ث ، وما المقبضان اللذان يمكنك تدويرهما لجعلها أنعم؟
الخطوة 3: المشاهد وحلقة المحرك
كائن واحد ارتداد. كائنات عدة على شبكة واحدة, تُخطى معًا بمعدل ثابت, حركة. تضيف الخطوة 3 Scene (الشبكة + الكائنات) وEngine (مشغِّل الخطوة الزمنية الثابتة).
3.1 قدِّم مشهدًا إلى نص
👟 تلميح البداية : اكتب Scene.render() تعيد قائمة سلاسل ، شبكة مملوءة بنقاط مع ختم كل كائن عند خليته (المقرّبة).
# main.py (continued)
class Scene:
def __init__(self, W=30, H=10):
self.W, self.H = W, H
self.sprites = []
def add(self, sprite):
sprite.W, sprite.H = self.W, self.H
self.sprites.append(sprite)
return self
def step(self, dt):
for sprite in self.sprites:
sprite.update(dt)
def render(self):
grid = [["."] * self.W for _ in range(self.H)]
for sprite in self.sprites:
gx, gy = int(sprite.x + 0.5), int(sprite.y + 0.5)
grid[gy][gx] = sprite.ch
return ["".join(row) for row in grid]
scene = Scene()
scene.add(Sprite("o", 0.0, 5.0, vx=4.0))
print("\n".join(scene.render()))
int(x + 0.5) هو قصّ التقريب-نصف-لأعلى: العوامات على حدّ الجدار تهبط على أقرب خلية حتميًا. Scene.add تُسند W/H الخاصة بها لكل كائن كي تطابق حدود الارتداد اللوحة دائمًا, مهما بُني الكائن به.
🎯 الناتج المتوقع :
..............................
..............................
..............................
..............................
..............................
o.............................
..............................
..............................
..............................
..............................
🩹 إذا لم يعمل : إن كان o في مكان آخر, فـy له ليست 5.0. إن أظهرت الشبكة 10 صفوف من 30 نقطة فالمشهد سليم ، هذه هي اللوحة الفارغة.
3.2 المحرك بخطوة زمنية ثابتة
👟 تلميح البداية : اكتب Engine(scene, fps=8) يشغِّل play(frames) بخطوة dt = 1/fps للمشهد ويعيد قائمة إطارات مقدّمة.
# main.py (continued)
class Engine:
def __init__(self, scene, fps=8):
self.scene = scene
self.fps = fps
self.dt = 1.0 / fps
def play(self, frames):
out = []
for _ in range(frames):
self.scene.step(self.dt)
out.append(self.scene.render())
return out
scene = Scene().add(Sprite("o", 0.0, 5.0, vx=4.0)).add(Sprite("*", 15.0, 2.0, vy=2.0))
frames = Engine(scene).play(12)
print("\n".join(frames[4]))
print("-" * 30)
print("\n".join(frames[11]))
play هي البكرة كلها: fps يثبّت dt, فـ8 إطارات = ثانية واحدة, وإعادة عرض المشهد نفسه بنفس المعاملات تنتج نفس الإطارات ، حتمية يمكنك اختبارها. الإطار 5 قبل مهلة مباشرة والإطار 12 لحظة علامة فارقة لكلا الكائنين.
🎯 الناتج المتوقع : الإطار 5 (frames[4]) يُظهر o في العمود 3 (بعد 4 × 0.5 = 2.0 → 2.5 → يَقرُب إلى 3) و* في الصف 3; والإطار 12 (frames[11]) يُظهر o في العمود 6 و* في الصف 5.
🩹 إذا لم يعمل : إن تداخل الكائنان في خلية غير متوقعة, فلأحدهما تناقض اتجاه سرعة. إن عادت الإطارات قديمة, فـscene.step يحوّر نسخة من المشهد, لا نفس الكائن.
3.3 تحقّق من المحرك
✅ قائمة التحقق
- ✅
play(12)مع المشهد أعلاه يعيد 12 إطارًا; ويطابق الإطاران 5 و12 الأعمدة/الصفوف المتوقعة. - ✅
Engine(scene, fps=8).dt == 0.125. - ✅ تشغيل
playمرتين على مشهد طازج يعيد إطارات متطابقة بايتًا-ببايت.
🤔 سؤال (أسئلة) سقراطي(ة) :
dtتساوي1/fps, لكن الحلقة تخطو المشهد ثم تطبع. بعد أن خطوت, هل الإطار 1 «الحالة بعد 0.125s» أم «عند الزمن 0»؟ اختر الدلالة وبرّر خلل الواحد-بالواحد الذي استقررت عليه.- يعيد المحرك الإطارات كقائمة ولا يطبعها أبدًا. لماذا البيانات (الإطارات) هي المنتَج هنا, والشاشة مجرد مستهلك ، وما الذي يتيح لك ذلك الاقتران لاستبداله لاحقًا؟
الخطوة 4: مسارات مفاتيح الإطارات
تعطيك السرعة خطوطًا مستقيمة وارتدادات. الحركة الحقيقية تكسّرها إلى مفاتيح إطارات ، أوضاع عند لحظات مختارة ، وتملأ ما بينها باستقراء ميسّر. تضيف الخطوة 4 متتبع المسار.
4.1 عيّن على طول مسار
👟 تلميح البداية : اكتب Keyframed(ch, keys) حيث keys قائمة توقف (t, (x, y)); يجد sample(t) المقطع الذي يحوي t وينعم عبره.
# main.py (continued)
class Keyframed:
def __init__(self, ch, keys):
self.ch = ch
self.keys = keys
self.x, self.y = keys[0][1]
def sample(self, t):
for i in range(len(self.keys) - 1):
t0, p0 = self.keys[i]
t1, p1 = self.keys[i + 1]
if t0 <= t <= t1:
u = smoothstep((t - t0) / (t1 - t0))
self.x = lerp(p0[0], p1[0], u)
self.y = lerp(p0[1], p1[1], u)
return (self.x, self.y)
return self.keys[-1][1]
node = Keyframed("A", [(0.0, (0, 0)), (1.0, (10, 2)), (2.0, (10, 8))])
print("t=0.5 ", node.sample(0.5))
print("t=1.0 ", node.sample(1.0))
print("t=2.0 ", node.sample(2.0))
يجد مسح المقطع مفتاحي الإطار اللذين يطوّقان t, ويعيد قياس t داخل ذلك المقطع (u), وينعّم u, ويستقرئ الإحداثيين معًا. المسار بيانات ، قائمة (زمن, موضع) ، وsample هي الدالة النقية التي تحوّل الزمن إلى وضع. بعد t=1.0 تنحني الرحلة من اليمين الحركة إلى الأسفل, ويتولى sample تسليم العصا.
🎯 الناتج المتوقع :
t=0.5 (5.0, 1.0)
t=1.0 (10.0, 2.0)
t=2.0 (10.0, 8.0)
🩹 إذا لم يعمل : إن أعاد t=0.5 قيمة (5.0, 0.0), فعبرها مقطع y القابل مبكرًا. إن أخطأت عيّنات بعد t=2.0, فـsample تسقط إلى self.keys[-1][1] فقط حين لا يجد الحلقة مقطعًا ، أكّد أن زمن مفتاح الإطار الأخير 2.0, لا < 2.0.
4.2 قدِّم مسارًا كبكرة
👟 تلميح البداية : الدور t = 0 … 2 بخطوة dt للمحرك, وعيّن المسار, واختم العقدة على شبكة طازجة, واجمع الإطارات.
# main.py (continued)
frames = []
for f in range(17):
_x, _y = node.sample(f * 0.125)
grid = [["."] * 30 for _ in range(10)]
grid[int(_y + 0.5)][int(_x + 0.5)] = node.ch
frames.append(["".join(r) for r in grid])
print("\n".join(frames[0]))
print("-" * 30)
print("\n".join(frames[16]))
الإطار 0 هو الوضع عند t=0: A في الزاوية العليا اليسرى. الإطار 17 هو t=2.0: A عند الصف 8, العمود 10. لأن sample نوّمت كلا المقطعين, يتمهل العقدة عند الزوايا وينطلق عبر المستقيمات.
🎯 الناتج المتوقع : الإطار 0 يحوي A في الزاوية العليا اليسرى; الإطار 16 يحوي A في الصف 8 (من 0–9), العمود 10.
🩹 إذا لم يعمل : إن لم تغادر A الزاوية العليا اليسرى أبدًا, فدُوِّرت sample بـt كفهرس إطار لا كـf * dt. إن هبطت عند (10, 2) وتوقفت, فتجاوز زمن نهاية المقطع الثاني مدى t للحلقة.
4.3 تحقّق من المسار
✅ قائمة التحقق
- ✅
sample(0.5)على المسار ثنائي المقطع يعيد(5.0, 1.0)، منتتصف المقطع الأول الميسَّر. - ✅ يقع
sample(1.5)على المقطع الثاني (بين(10, 2)و(10, 8)). - ✅ عينة ما بعد آخر مفتاح إطار تعيد الوضع النهائي, بلا تحطم.
🤔 سؤال (أسئلة) سقراطي(ة) :
- لا يحمل المسار سرعات ، أزمنة وأوضاع فقط. لماذا مفتاح الإطار بوضع-فقط أسهل تأليفًا من وضع بسرعة-فقط, وما المقايضة لحركة تريد فيها حلاقة سرعة دخول صريحة؟
- يُطبَّق smoothstep لكل مقطع, فيتمهل العقدة في طرفي الرحلة كلها. راقب الزاوية عند
t=1.0: هل تتحرك قط بأقصى سرعة, وهل يطابق ذلك كيف تقصّ كاميرا حقيقية بين اللقطات؟
الخطوة 5: صدّر البكرة
قائمة شبكات في الذاكرة جيدة; مجلد إطارات مرقّمة تسليم. تكتب الخطوة 5 الإطارات وتعيد تجميعها كشريط أفلام.
5.1 احفظ الإطارات إلى ملفات
👟 تلميح البداية : استخدم pathlib لكتابة كل إطار كـframe_000.txt, مبطنًا لثلاثة أرقام, وأعِد العدّ.
# main.py (continued)
import pathlib
def save_frames(frames, outdir):
outdir = pathlib.Path(outdir)
outdir.mkdir(exist_ok=True)
for i, frame in enumerate(frames):
(outdir / f"frame_{i:03d}.txt").write_text("\n".join(frame) + "\n")
return len(frames)
count = save_frames(frames, "reel")
print("wrote", count, "files")
print(list(pathlib.Path("reel").glob("frame_*.txt"))[:3])
f"frame_{i:03d}" هو التبطين الصفري الذي يجعل الملفات تُرتَّب صحيحًا (frame_009 قبل frame_010), فأي glob أو ls يعيد إنتاج الترتيب الزمني. يسمح العدّ المُعاد لخط أنابيب بالتحقق من الكتابة: 17 إطارًا دخلًا, 17 ملفًا خرجًا.
🎯 الناتج المتوقع :
wrote 17 files
[PosixPath('reel/frame_000.txt'), PosixPath('reel/frame_001.txt'), PosixPath('reel/frame_002.txt')]
🩹 إذا لم يعمل : إن قال تشغيل ثانٍ «17 ملفًا فعلًا», فـmkdir(exist_ok=True) مفقود (أو إطارات قديمة باقية وتتضاعف). إن كان glob فارغًا, فمجلد العمل الحالي يختلف عن outdir ، تحقق إلى أي مجلد كتب save_frames فعلًا.
5.2 أعد تجميع شريط أفلام
👟 تلميح البداية : اكتب read_reel(outdir) تحمّل الإطارات المرقّمة بالترتيب وتصلها بمحدّدات | كي تُظهر لمحة الحركة عبر الزمن.
# main.py (continued)
def read_reel(outdir):
outdir = pathlib.Path(outdir)
files = sorted(outdir.glob("frame_*.txt"))
frames = [f.read_text().splitlines() for f in files]
rows_in = len(frames[0])
return [" ".join(frames[i][row] for i in range(len(frames)))
for row in range(rows_in)]
film = read_reel("reel")
print("\n".join(film))
ينقل شريط الأفلام الصفوف: صف كل إطار أعلى على السطر 1, ثم صفّ كل إطار التالي على السطر 2 ، فتُعرض بكرة من 17 إطارًا كشريط عريض يمكنك تمريره أفقياً ورؤية النقطة تنتقل من اليسار إلى اليمين. sorted على الأسماء مبطنّة-صفرًا يضمن ترتيب الإطارات دون منطق ترتيب من عندك.
🎯 الناتج المتوقع : شريط أفلام من 10 صفوف وحوالي 510 عمودًا ينزلق فيه A من أقصى اليسار إلى أقصى اليمين عبر المقاطع, مع محددات تحفظ الإطارات منفصلة.
🩹 إذا لم يعمل : إن جاءت الإطارات بترتيب مشوّش, فسُمّيت الملفات بلا تبطين-صفر ووضع sorted frame_10 قبل frame_2. إن انحرفت خطوط كل إطار, فأسقط splitlines سطرًا جديدًا زائدًا وبطّن الصف الأخير بشكل غير متساوٍ.
5.3 تحقّق من التصدير
✅ قائمة التحقق
- ✅ يُعيد
save_frames17 ويكتب 17 ملفًا باسمframe_000.txt…frame_016.txt. - ✅ يعيد
read_reelإنجابframes[0]وframes[16]من القرص بالضبط. - ✅ تغيير سرعة كائن يغيّر ملفات الإطارات, مثبتًا أن البكرة تعكس الحالة لا فنًا مكوّدًا صلبًا.
🤔 سؤال (أسئلة) سقراطي(ة) :
- شريط الأفلام عرض شظية زمنية. ما المعلومات التي يريك إياها عن الحركة يخفيها التكديس إطار-بإطار ، وأي أسلوب حركة (دوران, تكبير) لن يلتقطه قط شريط صفوف ثنائي الأبعاد؟
- يكتب التصدير ملفات نصية يمكنك تسليمها لأداة غير Python. ما «صيغة التبادل المفتوحة» المكافئة في أداة الفيديو المفضلة لديك, وما قيمة إبقاء ناتج المحرك في صيغة لا يحتاج أي شيء آخر في مجموعتك لترجمتها؟
⚠️ مآزق شائعة
- قسمة صحيحة في التخفيف.
t / (t1 - t0)في Python 3 قسمة عائمة ، لكنt // (t1 - t0)أو وسائطintكاملة تقتطع بصمت وتجمّد منحناك. غذِّ عوامات في مساعدات الرياضيات. - التقريب-نصف-لأعلى مقابل تقريب المصرفي.
int(x + 0.5)يقرّب.5لأعلى دائمًا;round(x)في Python يقرّب.5إلى الزوجي, فكائن عندx=2.5يهبط عند2معroundوعند3معint(x+0.5)، وانجراف العوامات يجعل ذلك غير حتمي في البئر. اختر واحدًا وأبقه في كل مكان. - قصّ الجدران المتتالي. إذا استخدم فحص الارتداد
>=/<=على القيمة المقصوصة كل إطار, ينقلب كائن يستند عند جدار سرعته كل تحديث ويهتز إلى الأبد. اشترط عبور الحد أو افحص الموضع قبل القصّ. - خلل الواحد في الإطارات.
for f in range(17)ينتج 17 إطارًا عبرt = 16×dt; لتغطيةt=0حتىt=2.0شاملة تحتاج 17 خطوة, لا 16. قرّر هل تعدّ الإطارات خطوات زمنية أم إطارات ساعة-حائط. - تصديرات غير مرتبة. أسماء ملفات بلا تبطين-صفر ترتّب
frame_10قبلframe_2. بطّن لعرض ثابت (:03d) أو يتبلد شريط الأفلام. - تحوير المشهد داخل play. يجب أن يغيّر
scene.stepحالة الكائن في مكانها; إعادة إنشاء المشهد لكل إطار تفقد السرعات والارتدادات إلى الأبد.
ما بنيته للتو
محرك حركة نصي أولًا: رياضيات تخفيف, وكائنات مدفوعة بالسرعة مع ارتداد جدران, وحلقة عرض بخطوة زمنية ثابتة, ومسارات بمفاتيح إطارات, وبكرة مبنية على ملفات. الفكرة الجوهرية هي أن الحركة يقررها دوال صغيرة قابلة للتركيب ، clamp تحرس الحدود, وlerp يسافر, وsmoothstep يضيف شخصية, وكل صف يغلّف أمرًا كحالة. أطّر أي مشكلة حركة كـ«أي رقم أنعّم, ونحو أي غاية» وهذه القطع الخمس تجيب ، نفس الشكل يقود انتقالات CSS ومشيات الكائنات في الألعاب ونزول الكاميرا في الفيديو.
شغّل نسخة أكمل دون أي إعداد محلي
examples/animation-engine/ في مستودع الدورة هو المحرك كاملًا كدفتر ، ارتدادات الكائنات, ومسار مفاتيح الإطارات الميسَّر, وتصدير شريط الأفلام, قابلة للتشغيل في Colab/Kaggle/Binder. استنسخ المستودع أو افتحه في Codespace.
إلى أين تذهب من هنا
- أضف طبقة «تحريف الزمن»: بدل
dtعام واحد, أعطِ كل كائن مضاعفspeedخاصًا كي ينزلق*بتكاسل بينما يندفعo. - نَمذج ارتدادًا مرنًا لكائنين ، حين يصطدمان, تبادل السرعات وأضف تمايل
vxللضغط-والتمدد. - مدّد
Keyframedليحمل دالة تخفيف لكل مقطع (خطية للساق الأولى, smoothstep للثانية) كجزء من بيانات مفتاح الإطار. - اكتب الإطارات كصور PPM (P6) وهادها في GIF بكاتب Python نقي صغير, أو غذِّ شريط الأفلام في سجل التراجع لطرفيتك كـ«فيلم».
شارك مشروعك مع الصف
بنيت شيئًا تفخر به؟ examples/student-projects/ معرض لمشاريع قدّمها طلاب آخرون ، وREADME الخاص به يحوي شرحًا كاملًا وودودًا للمبتدئين لإضافة مشروعك عبر pull request, حتى لو لم تستخدم git من قبل قط: عمل fork للمستودع, وإنشاء فرع, وتثبيت ملفاتك, وفتح الـ PR, خطوة بخطوة. لا يُفترض أي خبرة سابقة بـ git.
مرحبًا بك في كتابة Python خارج المتصفح. 🎓
أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.