
جولة الاستوديو
مضيف من القصب فيه خمس غرف تلعبون فيها. مكتبة واحدة هي three.js، وكل ما عداها برمجة كتبناها له.
- المجالات
- برمجيات
- من عمل عليه
- فريق نابو ستوديوز
- متى
- من آب إلى تشرين الثاني ٢٠٢٦
- أين
- بغداد، العراق
- صُنعت باستخدام
- three.js، ووكلاء برمجة بالذكاء الاصطناعي يوجّههم فريقنا
معرض أعمال يُسمح فيه باللمس
أغلب معارض الأعمال تطلب منكم أن تنظروا، وهذا يدعوكم إلى اللعب، وقد صنعناه لمن يحب الاستكشاف. هو قاعة من القصب المربوط تسمّيها الجولة المضيف، لها خمسة أبواب، وخلف كل باب غرفة هي لعبة تعرض صنعة واحدة: غرفة الرسم، وستوديو التصوير، وغرفة الموشن، وغرفة الكود، وغرفة السيرفرات.
هي النقرة الثانية. تبقى الصفحة الرئيسية خفيفة، وتنتظر الجولة على بعد نقرة من يريد أن يرى أكثر، فيبقى ثقلها كله في صفحتها.
وليس فيها ما يجب إنهاؤه. تمشون في المضيف، وتختارون باباً، وتلعبون في الغرفة، ثم تخرجون.

غرفة الرسم. 
ستوديو التصوير. 
غرفة الموشن. 
غرفة الكود. 
غرفة السيرفرات.
أخطأنا الطريق مرات قبل أن نجد هذه الجولة
استغرق الوصول إلى الجولة شهرين وتصوّرات عدة. بدأت في آب عرضاً مُرشَداً في خمس غرف، لكل غرفة أسلوبها الفني، وفي كل منها عرض عملي لخدمة من خدماتنا.
وفي أيلول صارت كل غرفة تُفتح على مشروع متخيَّل «قبل»، ويد الزائر تحوّله إلى «بعد». وبعدها بقليل كتب أحد مؤسسينا كلمات الجولة باللهجة العراقية، غرفةً غرفة، فصارت العربية هي الأصل والإنجليزية مرآتها.
وفي مطلع تشرين الأول ضاع الزوار بأربع طرق: كيف يتحركون، وماذا يفعلون في الغرفة، وأين هم، ولماذا هي لعبة أصلاً. فأجبنا بخريطة مراحل ومهامّ تكسب نجوماً. ولمّا لعبها أواب وهي نصف مبنية، بدت المهام غامضة وكثيرة الكلام وأقرب إلى الواجب المدرسي، وكسرت الخريطة الجولة، فصارت في اليوم نفسه إصلاحاً واحداً في كل غرفة.
وبعد أربعة أيام اختصرنا أكثر. كانت المهام ما تزال مفتعلة، وصارت الكلمات أشبه بمعرض ينقلب إعلاناً، فجعلنا الغرف ألعاباً لا شيء فيها يُنهى، ولم نُبقِ من الكلمات إلا أسماء الغرف وسطراً عند كل باب وسطراً في النهاية.
ثم بدت القطع الجديدة كأنها دخلت المضيف من لعبة أخرى، فأعدنا بناءها بإضاءة المضيف نفسه. ثم اختبرناها دون أن نكشف عنها: حاول مراجِعٌ لم يرها من قبل أن يميّزها في المضيف، وما ظلّ يميّزه بعد ثلاث دفعات من الإصلاح رفعناه إلى أواب ليحكم فيه.
ثم عادت كلمات قليلة حيث احتاجها الزائر. ففي ٢٧ تشرين الأول صار لكل مكان سطر واحد يقول ما يُفعل فيه، وصار الشيء الذي تلعبون به في كل غرفة يتوهّج حتى تستعملوه، فإذا استعملتموه اختفى السطر والتوهّج. وعلى الهاتف صارت السحبة الجانبية تمشي بكم في المضيف أيضاً، في اتجاه قراءة اللغة. وفي ١ تشرين الثاني صارت في أول المضيف بطاقة تقول لماذا صنعنا الجولة، وتختفي حين تمشون إلى الداخل.
three.js، ولا شيء غيرها
تستعمل الجولة مكتبة واحدة، three.js، لأساسيات الرسم ثلاثي الأبعاد في المتصفح: تحفظ المشهد والكاميرا، وتسلّم بطاقة الرسوميات المثلثات التي ترسمها.
كل ما ترونه وتسمعونه مصنوع بالبرمجة. لا نماذج ثلاثية الأبعاد، ولا صور للخامات، ولا ملفات صوت، ولا إضافات لمكتبة three.js، ولا محرّك ألعاب، ولا مكتبة للفيزياء أو للحركة.
وتركنا أيضاً الأدوات التي تُبنى عليها أغلب مواقع الأبعاد الثلاثة. قسنا React مع React Three Fiber وحدهما فبلغا ٢٩٤٫٣ كيلوبايت، وبلغ البديل المبني على Preact ١٣٥٫١ كيلوبايت. ثم إن React Three Fiber يجرّ three.js كلها معه، فيعجز البناء عن أن يُبقي ما تستعمله الجولة وحده. أما Preact فلا يظهر إلا في مكان واحد: النسخة المسطّحة من الجولة، للأجهزة التي لا تشغّل الأبعاد الثلاثة.
صمّمنا الجولة ووجّهنا كل مرحلة من مراحل العمل عليها. كتب وكلاء برمجة بالذكاء الاصطناعي كثيراً من الكود، ولم نعدّ شيئاً مما كتبوه منجزاً حتى اجتاز مواصفاتنا المكتوبة والاختبارات والحدود المقيسة في هذه الصفحة.
alias: {
react: "preact/compat",
"react-dom": "preact/compat",
"react-dom/client": "preact/compat/client",
},جولة على حمية صارمة
كل بايت ترسله الجولة يدفع ثمنه زائر، وكثيراً ما يكون على هاتف في بغداد، لذلك جعلنا لحجمها سقفاً. وإذا تجاوزته فشل البناء، فلا تسمن الجولة دون أن ينتبه أحد.
ولكي تبقى تحته، تخلو صفحة الجولة من شيفرة Next.js نفسها، وقد قسناها وحدها فبلغت نحو ١٩٠ كيلوبايت. تصل الصفحة مستنداً عادياً، وتُحمَّل الجولة برنامجاً مستقلاً، ولا تُحمَّل كل غرفة إلا حين تقتربون من بابها.
بدأ السقف عند ٢٥٠ كيلوبايت. ويومَ ١١ آب ٢٠٢٦، حين لم تعد غرف منجزة تتسع له، رفعناه عن قصد إلى ٤٠٠ كيلوبايت، وكتبنا بجانب الرقم قاعدة: ما لا يتسع يُقطع.
وعلى هذا الموقع وضعنا السقف من أول قياس للجولة هنا، وزدنا عليه العُشر، وهو يحسب الغرف كلها.
// Set on 2026-10-14 from the first measurement in this repo: 302.5 KB for /en/studio/ and 302.5 KB for
// /ar/studio/, the walk's whole closure; the larger value times 1.1, rounded up, is 333.
export const STUDIO_BUDGET_KB = 333;مرسومة بالبرمجة، حتى الورق
ليس في الجولة صورة واحدة تُحمَّل من ملف. جدران المضيف وحُصر القصب والغرف وأثاثها أشكال نبنيها بالبرمجة، وضوؤها ولونها تحسبهما برامج صغيرة اسمها المظلِّلات، كتبناها لكل غرفة، وتعمل على بطاقة الرسوميات لكل بكسل.
خذوا ورق غرفة الرسم مثلاً. ليس خلفه مسح ضوئي لورق: تفاوت لونه وبقعه الباهتة ضوضاء يصنعها قانون رياضي، في أربع طبقات، كل طبقة أدق وأخف من التي قبلها.
حتى شاشة التحميل مرسومة. ترصّ مربعات علامتنا الـ١٠٨ واحداً واحداً من البيانات نفسها التي تُرسم منها العلامة.
float fbm(vec2 p){
float s = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) { s += a * vnoise(p); p *= 2.07; a *= 0.5; }
return s;
}بدت كلعبة رعب
في أوائل أيلول مشينا في الجولة المنجزة والصوت يعمل، فبدت كلعبة رعب. وحين قسناها كان السبب واضحاً: صوت كل غرفة ضوضاء مفلترة بلا نغمة فيها، وكلها في نطاق واحد منخفض معتم، فإذا أطفأتم الصورة لم تميّزوا غرفة من غرفة.
والأجواء ليست موسيقى، فصارت للجولة مقطوعة. هي قطعة واحدة في سلّم فا الكبير، لكل غرفة فيها تآلف بالترتيب الذي تمرّون به: ري الصغير في المضيف، ثم عبر الغرف إلى دو عند غرفة الكود، وهو تآلف يريد أن يُحلّ، ثم يبقى معلّقاً في غرفة السيرفرات، ويُحلّ إلى فا حين تُفتح الأبواب البعيدة.
وبين غرفة وأخرى لا يتحرك إلا صوت أو صوتان من الأصوات الثلاثة، كلٌّ إلى أقرب نغمة في التآلف الجديد. وبهذا يُسمع الانتقال من غرفة إلى غرفة استمراراً للموسيقى نفسها لا قطعاً لها.
وبعد يومين بدت التآلفات وحدها كالتشويش، فصار للمقطوعة لحن كُتب نغمةً نغمة ولم يُولَّد، فلا يبدو أبداً كآلة تصعد على تآلف. وبعد أربعة أيام أخرى صار لجملة كل غرفة جواب، فمن يطيل البقاء يسمع سؤالاً وردّه لا حلقة تتكرر.
حتى طبقة الصوت قسناها. حين كانت المقطوعة أخفض بأوكتاف، كان ٨٢ في المئة من ثقلها تحت ٢٠٠ هرتز، حيث لا تكاد مكبرات الحاسوب والهاتف الصغيرة تُخرج شيئاً، فرفعناها. ولا نزيح أصوات الغرف نفسها لتقع على الإيقاع أبداً: كل صوت منها ينتمي إلى شيء يحدث في الصورة، والموسيقى تنحني حوله.
التسجيلان أدناه من البرمجة نفسها التي تعزف في الجولة، سجّلناهما وهي تعزف. وليس فيهما الأصوات التي تصدرها الغرف حين تلعبون فيها.
export const CHORDS: Record<string, readonly [number, number, number]> = {
// …
corridor: [A3, D4, F4],
// …
code: [C4, E4, G4],
// …
server: [C4, F4, G4],
// …
closing: [C4, F4, A4],
};زيارة أولى تُحتمل
ويومَ ٢٠ أيلول قسنا الجولة على حاسوب محمول. كانت تحافظ على ٦٠ إطاراً في الثانية في كل غرفة، ومع ذلك كانت الزيارة الأولى تتجمّد: توقفت الصفحة ١٣٫٦ ثانية في المجموع، مقابل نحو ٣ إلى ٤ ثوانٍ في الزيارتين التاليتين، بينما تجهّز بطاقة الرسوميات مظلِّلات كل غرفة، وكلما طال المظلِّل طال الانتظار.
ولو جدولنا هذا العمل بطريقة أخرى لانتقل الانتظار ولم يختفِ، فذهبنا إلى سببه.
السبب هو المترجم الذي يستعمله Chrome على Windows، فهو ينسخ كل دالة في كل موضع تُستدعى فيه. ويومَ ٢٤ أيلول أعدنا كتابة أثقل المظلِّلات لنخفّف ما يحمله، بالبكسلات نفسها تماماً، فنقص زمن تجهيز ستوديو التصوير ٤٥ في المئة، وزمن غرفة الرسم ٢٣ في المئة.
وصارت غرفة الرسم تبدأ تجهيزها والزائر ما يزال في المضيف، فنزل تجمّدها عند الوصول من نحو أربع ثوانٍ إلى ما بين ربع ثانية و١٫٦ ثانية.
وبقيت الغرف تحتاج لحظة لتُبنى عند الوصول إليها: نحو ١٨٠ ملّي ثانية لغرفة السيرفرات و١٠٠ لغرفة الكود، يحسّها الواقف في المضيف تعثّراً. ومنذ ٢٦ تشرين الأول تُبنى هاتان الغرفتان على دفعات، قطعة في كل إطار، فنزل أسوأ إطار عند الوصول في اختبار الهاتف من نحو ١٦٦ ملّي ثانية إلى ٦٦.
vec4 hash21x4(vec2 i){
vec4 x = fract(vec4(i.x, i.x + 1.0, i.x, i.x + 1.0) * 127.31);
vec4 y = fract(vec4(i.y, i.y, i.y + 1.0, i.y + 1.0) * 311.7);
vec4 d = x * (x + 34.23) + y * (y + 34.23);
return fract((x + d) * (y + d));
}أين وصلت
تعمل الجولة بالإنجليزية والعربية، ووزنها على هذا الموقع ٣٠٤٫٥ كيلوبايت بغرفها كلها، تحت سقف ٣٣٣ كيلوبايت.
وحين قسناها على حاسوب الاختبار في ١٤ تشرين الأول، بحجم سطح المكتب، عملت بين ٥٦ و٦٠ إطاراً في الثانية في المضيف وغرفه.
جرّبوها والصوت مفتوح.
